|
|
@@ -226,6 +226,15 @@ private final class StoreKitCoordinator {
|
|
226
|
226
|
}
|
|
227
|
227
|
|
|
228
|
228
|
final class ViewController: NSViewController {
|
|
|
229
|
+ private enum PaywallFooterAction {
|
|
|
230
|
+ case manageSubscription
|
|
|
231
|
+ case restorePurchase
|
|
|
232
|
+ case continueWithFreePlan
|
|
|
233
|
+ case privacyPolicy
|
|
|
234
|
+ case support
|
|
|
235
|
+ case termsOfServices
|
|
|
236
|
+ }
|
|
|
237
|
+
|
|
229
|
238
|
private struct GoogleProfileDisplay {
|
|
230
|
239
|
let name: String
|
|
231
|
240
|
let email: String
|
|
|
@@ -249,11 +258,13 @@ final class ViewController: NSViewController {
|
|
249
|
258
|
private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
|
|
250
|
259
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
251
|
260
|
private var paywallWindow: NSWindow?
|
|
|
261
|
+ private weak var paywallOverlayView: NSView?
|
|
252
|
262
|
private let paywallContentWidth: CGFloat = 520
|
|
253
|
263
|
private let launchWindowLeftOffset: CGFloat = 80
|
|
254
|
264
|
private var selectedPremiumPlan: PremiumPlan = .monthly
|
|
255
|
265
|
private var paywallPlanViews: [PremiumPlan: NSView] = [:]
|
|
256
|
266
|
private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
|
|
|
267
|
+ private var paywallFooterActionByView = [ObjectIdentifier: PaywallFooterAction]()
|
|
257
|
268
|
private weak var paywallOfferLabel: NSTextField?
|
|
258
|
269
|
private weak var paywallContinueLabel: NSTextField?
|
|
259
|
270
|
private weak var paywallContinueButton: NSView?
|
|
|
@@ -745,6 +756,10 @@ private extension ViewController {
|
|
745
|
756
|
}
|
|
746
|
757
|
|
|
747
|
758
|
private func openManageSubscriptions() {
|
|
|
759
|
+ if let appStoreURL = URL(string: "macappstore://apps.apple.com/account/subscriptions"),
|
|
|
760
|
+ NSWorkspace.shared.open(appStoreURL) {
|
|
|
761
|
+ return
|
|
|
762
|
+ }
|
|
748
|
763
|
guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else {
|
|
749
|
764
|
showSimpleAlert(title: "Unable to Open Subscriptions", message: "The subscriptions URL is invalid.")
|
|
750
|
765
|
return
|
|
|
@@ -752,6 +767,27 @@ private extension ViewController {
|
|
752
|
767
|
openInDefaultBrowser(url: url)
|
|
753
|
768
|
}
|
|
754
|
769
|
|
|
|
770
|
+ private func openRestoreSubscriptionPage() {
|
|
|
771
|
+ let fallbackURL = "https://support.apple.com/en-us/108096"
|
|
|
772
|
+ let restoreURL = (Bundle.main.object(forInfoDictionaryKey: "RestoreSubscriptionURL") as? String) ?? fallbackURL
|
|
|
773
|
+ guard let url = URL(string: restoreURL) else {
|
|
|
774
|
+ if let fallback = URL(string: fallbackURL) {
|
|
|
775
|
+ NSWorkspace.shared.open(fallback)
|
|
|
776
|
+ }
|
|
|
777
|
+ return
|
|
|
778
|
+ }
|
|
|
779
|
+ NSWorkspace.shared.open(url)
|
|
|
780
|
+ }
|
|
|
781
|
+
|
|
|
782
|
+ private func performRestorePurchases() {
|
|
|
783
|
+ Task { [weak self] in
|
|
|
784
|
+ guard let self else { return }
|
|
|
785
|
+ let message = await self.storeKitCoordinator.restorePurchases()
|
|
|
786
|
+ self.refreshPaywallStoreUI()
|
|
|
787
|
+ self.showSimpleAlert(title: "Restore Purchases", message: message)
|
|
|
788
|
+ }
|
|
|
789
|
+ }
|
|
|
790
|
+
|
|
755
|
791
|
private func shareAppURL() -> URL? {
|
|
756
|
792
|
if let configured = Bundle.main.object(forInfoDictionaryKey: "AppShareURL") as? String {
|
|
757
|
793
|
let trimmed = configured.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
@@ -839,6 +875,7 @@ private extension ViewController {
|
|
839
|
875
|
settingsActionByView.removeAll()
|
|
840
|
876
|
paywallPlanViews.removeAll()
|
|
841
|
877
|
premiumPlanByView.removeAll()
|
|
|
878
|
+ paywallFooterActionByView.removeAll()
|
|
842
|
879
|
paywallPriceLabels.removeAll()
|
|
843
|
880
|
paywallSubtitleLabels.removeAll()
|
|
844
|
881
|
paywallContinueLabel = nil
|
|
|
@@ -860,12 +897,7 @@ private extension ViewController {
|
|
860
|
897
|
private func handleSettingsAction(_ action: SettingsAction, sourceView: NSView? = nil, clickLocationInSourceView: NSPoint? = nil) {
|
|
861
|
898
|
switch action {
|
|
862
|
899
|
case .restore:
|
|
863
|
|
- Task { [weak self] in
|
|
864
|
|
- guard let self else { return }
|
|
865
|
|
- let message = await self.storeKitCoordinator.restorePurchases()
|
|
866
|
|
- self.refreshPaywallStoreUI()
|
|
867
|
|
- self.showSimpleAlert(title: "Restore Purchases", message: message)
|
|
868
|
|
- }
|
|
|
900
|
+ performRestorePurchases()
|
|
869
|
901
|
case .rateUs:
|
|
870
|
902
|
openRateUsDestination()
|
|
871
|
903
|
case .support:
|
|
|
@@ -1000,10 +1032,22 @@ private extension ViewController {
|
|
1000
|
1032
|
}
|
|
1001
|
1033
|
|
|
1002
|
1034
|
private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
|
|
|
1035
|
+ if !Thread.isMainThread {
|
|
|
1036
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
1037
|
+ self?.showPaywall(upgradeFlow: upgradeFlow, preferredPlan: preferredPlan)
|
|
|
1038
|
+ }
|
|
|
1039
|
+ return
|
|
|
1040
|
+ }
|
|
1003
|
1041
|
paywallUpgradeFlowEnabled = upgradeFlow
|
|
1004
|
1042
|
if let preferredPlan {
|
|
1005
|
1043
|
selectedPremiumPlan = preferredPlan
|
|
1006
|
1044
|
}
|
|
|
1045
|
+ if let existingOverlay = paywallOverlayView {
|
|
|
1046
|
+ refreshPaywallStoreUI()
|
|
|
1047
|
+ existingOverlay.alphaValue = 1
|
|
|
1048
|
+ view.addSubview(existingOverlay, positioned: .above, relativeTo: nil)
|
|
|
1049
|
+ return
|
|
|
1050
|
+ }
|
|
1007
|
1051
|
if let existing = paywallWindow {
|
|
1008
|
1052
|
refreshPaywallStoreUI()
|
|
1009
|
1053
|
animatePaywallPresentation(existing)
|
|
|
@@ -1013,33 +1057,30 @@ private extension ViewController {
|
|
1013
|
1057
|
}
|
|
1014
|
1058
|
|
|
1015
|
1059
|
let content = makePaywallContent()
|
|
1016
|
|
- let controller = NSViewController()
|
|
1017
|
|
- controller.view = content
|
|
1018
|
|
-
|
|
1019
|
|
- let panel = NSPanel(
|
|
1020
|
|
- contentRect: NSRect(x: 0, y: 0, width: 640, height: 820),
|
|
1021
|
|
- styleMask: [.titled, .closable, .fullSizeContentView],
|
|
1022
|
|
- backing: .buffered,
|
|
1023
|
|
- defer: false
|
|
1024
|
|
- )
|
|
1025
|
|
- panel.title = "Get Premium"
|
|
1026
|
|
- panel.titleVisibility = .hidden
|
|
1027
|
|
- panel.titlebarAppearsTransparent = true
|
|
1028
|
|
- panel.isFloatingPanel = false
|
|
1029
|
|
- panel.level = .normal
|
|
1030
|
|
- panel.hidesOnDeactivate = true
|
|
1031
|
|
- panel.isReleasedWhenClosed = false
|
|
1032
|
|
- panel.delegate = self
|
|
1033
|
|
- panel.standardWindowButton(.closeButton)?.isHidden = true
|
|
1034
|
|
- panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
|
1035
|
|
- panel.standardWindowButton(.zoomButton)?.isHidden = true
|
|
1036
|
|
- panel.center()
|
|
1037
|
|
- panel.contentViewController = controller
|
|
1038
|
|
- panel.alphaValue = 0
|
|
1039
|
|
- panel.makeKeyAndOrderFront(nil)
|
|
1040
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
1041
|
|
- paywallWindow = panel
|
|
1042
|
|
- animatePaywallPresentation(panel)
|
|
|
1060
|
+ let overlay = NSView()
|
|
|
1061
|
+ overlay.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1062
|
+ overlay.wantsLayer = true
|
|
|
1063
|
+ overlay.layer?.backgroundColor = palette.pageBackground.withAlphaComponent(0.98).cgColor
|
|
|
1064
|
+ overlay.alphaValue = 0
|
|
|
1065
|
+ overlay.addSubview(content)
|
|
|
1066
|
+ view.addSubview(overlay, positioned: .above, relativeTo: nil)
|
|
|
1067
|
+ NSLayoutConstraint.activate([
|
|
|
1068
|
+ overlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
1069
|
+ overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
1070
|
+ overlay.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
1071
|
+ overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
1072
|
+
|
|
|
1073
|
+ content.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
|
|
|
1074
|
+ content.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
|
|
|
1075
|
+ content.topAnchor.constraint(equalTo: overlay.topAnchor),
|
|
|
1076
|
+ content.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
|
|
|
1077
|
+ ])
|
|
|
1078
|
+ paywallOverlayView = overlay
|
|
|
1079
|
+ NSAnimationContext.runAnimationGroup { context in
|
|
|
1080
|
+ context.duration = 0.20
|
|
|
1081
|
+ context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
1082
|
+ overlay.animator().alphaValue = 1
|
|
|
1083
|
+ }
|
|
1043
|
1084
|
|
|
1044
|
1085
|
Task { [weak self] in
|
|
1045
|
1086
|
guard let self else { return }
|
|
|
@@ -1071,6 +1112,12 @@ private extension ViewController {
|
|
1071
|
1112
|
}
|
|
1072
|
1113
|
|
|
1073
|
1114
|
@objc private func closePaywallClicked(_ sender: Any?) {
|
|
|
1115
|
+ if let overlay = paywallOverlayView {
|
|
|
1116
|
+ paywallOverlayView = nil
|
|
|
1117
|
+ paywallUpgradeFlowEnabled = false
|
|
|
1118
|
+ overlay.removeFromSuperview()
|
|
|
1119
|
+ return
|
|
|
1120
|
+ }
|
|
1074
|
1121
|
if let win = paywallWindow {
|
|
1075
|
1122
|
win.performClose(nil)
|
|
1076
|
1123
|
return
|
|
|
@@ -1087,18 +1134,35 @@ private extension ViewController {
|
|
1087
|
1134
|
|
|
1088
|
1135
|
@objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
|
|
1089
|
1136
|
guard let view = sender.view else { return }
|
|
|
1137
|
+ let action = paywallFooterActionByView[ObjectIdentifier(view)]
|
|
1090
|
1138
|
let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
|
|
1091
|
|
- let defaultURL = (Bundle.main.object(forInfoDictionaryKey: "AppLaunchPlaceholderURL") as? String) ?? "https://example.com/app-link-coming-soon"
|
|
1092
|
|
- let map: [String: String] = [
|
|
1093
|
|
- "Privacy Policy": (Bundle.main.object(forInfoDictionaryKey: "PrivacyPolicyURL") as? String) ?? defaultURL,
|
|
1094
|
|
- "Support": (Bundle.main.object(forInfoDictionaryKey: "SupportURL") as? String) ?? defaultURL,
|
|
1095
|
|
- "Terms of Services": (Bundle.main.object(forInfoDictionaryKey: "TermsOfServiceURL") as? String) ?? defaultURL
|
|
1096
|
|
- ]
|
|
1097
|
|
- if let urlString = map[text], let url = URL(string: urlString) {
|
|
1098
|
|
- openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
1139
|
+ guard let action else {
|
|
|
1140
|
+ showSimpleAlert(title: text, message: "\(text) tapped.")
|
|
1099
|
1141
|
return
|
|
1100
|
1142
|
}
|
|
1101
|
|
- showSimpleAlert(title: text, message: "\(text) tapped.")
|
|
|
1143
|
+ handlePaywallFooterAction(action)
|
|
|
1144
|
+ }
|
|
|
1145
|
+
|
|
|
1146
|
+ private func handlePaywallFooterAction(_ action: PaywallFooterAction) {
|
|
|
1147
|
+ switch action {
|
|
|
1148
|
+ case .manageSubscription:
|
|
|
1149
|
+ openManageSubscriptions()
|
|
|
1150
|
+ case .restorePurchase:
|
|
|
1151
|
+ openRestoreSubscriptionPage()
|
|
|
1152
|
+ case .continueWithFreePlan:
|
|
|
1153
|
+ closePaywallClicked(nil)
|
|
|
1154
|
+ case .privacyPolicy:
|
|
|
1155
|
+ openSettingsLink(infoKey: "PrivacyPolicyURL")
|
|
|
1156
|
+ case .support:
|
|
|
1157
|
+ openSettingsLink(infoKey: "SupportURL")
|
|
|
1158
|
+ case .termsOfServices:
|
|
|
1159
|
+ openSettingsLink(infoKey: "TermsOfServiceURL")
|
|
|
1160
|
+ }
|
|
|
1161
|
+ }
|
|
|
1162
|
+
|
|
|
1163
|
+ @objc private func paywallFooterButtonPressed(_ sender: NSButton) {
|
|
|
1164
|
+ guard let action = paywallFooterActionByView[ObjectIdentifier(sender)] else { return }
|
|
|
1165
|
+ handlePaywallFooterAction(action)
|
|
1102
|
1166
|
}
|
|
1103
|
1167
|
|
|
1104
|
1168
|
@objc private func paywallPlanClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
@@ -1141,8 +1205,13 @@ private extension ViewController {
|
|
1141
|
1205
|
if product.type == .nonConsumable {
|
|
1142
|
1206
|
return "\(pkrPrice) one-time purchase"
|
|
1143
|
1207
|
}
|
|
1144
|
|
- if let period = product.subscription?.subscriptionPeriod {
|
|
1145
|
|
- return "\(pkrPrice)/\(subscriptionUnitText(period.unit))"
|
|
|
1208
|
+ if let subscription = product.subscription {
|
|
|
1209
|
+ let billingText = "\(pkrPrice)/\(subscriptionUnitText(subscription.subscriptionPeriod.unit))"
|
|
|
1210
|
+ if let introOffer = subscription.introductoryOffer,
|
|
|
1211
|
+ introOffer.paymentMode == .freeTrial {
|
|
|
1212
|
+ return "Free for \(subscriptionPeriodText(introOffer.period)), then \(billingText)"
|
|
|
1213
|
+ }
|
|
|
1214
|
+ return billingText
|
|
1146
|
1215
|
}
|
|
1147
|
1216
|
return pkrPrice
|
|
1148
|
1217
|
}
|
|
|
@@ -1150,7 +1219,7 @@ private extension ViewController {
|
|
1150
|
1219
|
case .weekly:
|
|
1151
|
1220
|
return "PKR 1,100.00/week"
|
|
1152
|
1221
|
case .monthly:
|
|
1153
|
|
- return "Free for 3 Days then PKR 2,500.00/month"
|
|
|
1222
|
+ return "PKR 2,500.00/month (3 days free trial)"
|
|
1154
|
1223
|
case .yearly:
|
|
1155
|
1224
|
return "PKR 9,900.00/year (about 190.38/week)"
|
|
1156
|
1225
|
case .lifetime:
|
|
|
@@ -1177,6 +1246,22 @@ private extension ViewController {
|
|
1177
|
1246
|
}
|
|
1178
|
1247
|
}
|
|
1179
|
1248
|
|
|
|
1249
|
+ private func subscriptionPeriodText(_ period: Product.SubscriptionPeriod) -> String {
|
|
|
1250
|
+ let unit = subscriptionUnitText(period.unit)
|
|
|
1251
|
+ if period.value == 1 {
|
|
|
1252
|
+ return "1 \(unit)"
|
|
|
1253
|
+ }
|
|
|
1254
|
+ return "\(period.value) \(unit)s"
|
|
|
1255
|
+ }
|
|
|
1256
|
+
|
|
|
1257
|
+ private func freeTrialPackageText(for product: Product) -> String? {
|
|
|
1258
|
+ guard let introOffer = product.subscription?.introductoryOffer,
|
|
|
1259
|
+ introOffer.paymentMode == .freeTrial else {
|
|
|
1260
|
+ return nil
|
|
|
1261
|
+ }
|
|
|
1262
|
+ return "\(subscriptionPeriodText(introOffer.period)) free trial"
|
|
|
1263
|
+ }
|
|
|
1264
|
+
|
|
1180
|
1265
|
private func startStoreKit() {
|
|
1181
|
1266
|
storeKitStartupTask?.cancel()
|
|
1182
|
1267
|
storeKitStartupTask = Task { [weak self] in
|
|
|
@@ -1198,8 +1283,18 @@ private extension ViewController {
|
|
1198
|
1283
|
for (plan, label) in paywallSubtitleLabels {
|
|
1199
|
1284
|
let productID = PremiumStoreProduct.productID(for: plan)
|
|
1200
|
1285
|
guard let product = storeKitCoordinator.productsByID[productID],
|
|
1201
|
|
- let period = product.subscription?.subscriptionPeriod else { continue }
|
|
1202
|
|
- label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
|
|
|
1286
|
+ let period = product.subscription?.subscriptionPeriod else {
|
|
|
1287
|
+ if plan == .monthly {
|
|
|
1288
|
+ label.stringValue = "3 days free trial"
|
|
|
1289
|
+ }
|
|
|
1290
|
+ continue
|
|
|
1291
|
+ }
|
|
|
1292
|
+ let recurringText = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
|
|
|
1293
|
+ if let trialText = freeTrialPackageText(for: product) {
|
|
|
1294
|
+ label.stringValue = "\(recurringText) • \(trialText)"
|
|
|
1295
|
+ } else {
|
|
|
1296
|
+ label.stringValue = recurringText
|
|
|
1297
|
+ }
|
|
1203
|
1298
|
}
|
|
1204
|
1299
|
refreshSidebarPremiumButton()
|
|
1205
|
1300
|
refreshInstantMeetPremiumState()
|
|
|
@@ -2568,8 +2663,10 @@ private extension ViewController {
|
|
2568
|
2663
|
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
2569
|
2664
|
contentStack.orientation = .vertical
|
|
2570
|
2665
|
contentStack.spacing = 12
|
|
2571
|
|
- contentStack.alignment = .leading
|
|
|
2666
|
+ contentStack.distribution = .fill
|
|
|
2667
|
+ contentStack.alignment = .centerX
|
|
2572
|
2668
|
panel.addSubview(contentStack)
|
|
|
2669
|
+ let paywallLayoutWidth: CGFloat = 980
|
|
2573
|
2670
|
|
|
2574
|
2671
|
let topRow = NSStackView()
|
|
2575
|
2672
|
topRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -2577,73 +2674,109 @@ private extension ViewController {
|
|
2577
|
2674
|
topRow.alignment = .centerY
|
|
2578
|
2675
|
topRow.distribution = .fill
|
|
2579
|
2676
|
topRow.spacing = 10
|
|
2580
|
|
- topRow.addArrangedSubview(textLabel("Get Premium", font: NSFont.systemFont(ofSize: 24, weight: .bold), color: palette.textPrimary))
|
|
2581
|
|
- let topSpacer = NSView()
|
|
2582
|
|
- topSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
2583
|
|
- topRow.addArrangedSubview(topSpacer)
|
|
2584
|
|
- let closeButton = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
|
|
2585
|
|
- closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
2586
|
|
- closeButton.isBordered = false
|
|
2587
|
|
- closeButton.bezelStyle = .regularSquare
|
|
2588
|
|
- closeButton.wantsLayer = true
|
|
2589
|
|
- closeButton.layer?.cornerRadius = 14
|
|
2590
|
|
- closeButton.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
2591
|
|
- closeButton.layer?.borderColor = palette.inputBorder.cgColor
|
|
2592
|
|
- closeButton.layer?.borderWidth = 1
|
|
2593
|
|
- closeButton.font = typography.iconButton
|
|
2594
|
|
- closeButton.contentTintColor = palette.textSecondary
|
|
2595
|
|
- closeButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
|
|
2596
|
|
- closeButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
2597
|
|
- closeButton.onHoverChanged = { [weak closeButton, weak self] hovering in
|
|
2598
|
|
- guard let closeButton, let self else { return }
|
|
2599
|
|
- let base = self.palette.inputBackground
|
|
2600
|
|
- let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
|
|
2601
|
|
- let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
2602
|
|
- closeButton.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
2603
|
|
- closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
2677
|
+ topRow.addArrangedSubview(textLabel("Meetings Pro", font: NSFont.systemFont(ofSize: 27, weight: .bold), color: palette.textPrimary))
|
|
|
2678
|
+ var closeButton: HoverButton?
|
|
|
2679
|
+ if storeKitCoordinator.hasPremiumAccess {
|
|
|
2680
|
+ let button = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
|
|
|
2681
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2682
|
+ button.isBordered = false
|
|
|
2683
|
+ button.bezelStyle = .regularSquare
|
|
|
2684
|
+ button.wantsLayer = true
|
|
|
2685
|
+ button.layer?.cornerRadius = 14
|
|
|
2686
|
+ button.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
2687
|
+ button.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
2688
|
+ button.layer?.borderWidth = 1
|
|
|
2689
|
+ button.font = typography.iconButton
|
|
|
2690
|
+ button.contentTintColor = palette.textSecondary
|
|
|
2691
|
+ button.widthAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
2692
|
+ button.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
2693
|
+ button.onHoverChanged = { [weak button, weak self] hovering in
|
|
|
2694
|
+ guard let button, let self else { return }
|
|
|
2695
|
+ let base = self.palette.inputBackground
|
|
|
2696
|
+ let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
2697
|
+ let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
|
2698
|
+ button.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
|
2699
|
+ button.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
2700
|
+ }
|
|
|
2701
|
+ panel.addSubview(button)
|
|
|
2702
|
+ closeButton = button
|
|
2604
|
2703
|
}
|
|
2605
|
|
- topRow.addArrangedSubview(closeButton)
|
|
2606
|
|
- topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
2607
|
2704
|
contentStack.addArrangedSubview(topRow)
|
|
|
2705
|
+ topRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
2706
|
+
|
|
|
2707
|
+ let hero = roundedContainer(cornerRadius: 16, color: palette.sectionCard)
|
|
|
2708
|
+ hero.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2709
|
+ styleSurface(hero, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
2710
|
+ contentStack.addArrangedSubview(hero)
|
|
|
2711
|
+ hero.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
2712
|
+
|
|
|
2713
|
+ let heroBadge = textLabel("PREMIUM EXPERIENCE", font: NSFont.systemFont(ofSize: 10, weight: .bold), color: NSColor(calibratedRed: 0.42, green: 0.93, blue: 0.70, alpha: 1))
|
|
|
2714
|
+ hero.addSubview(heroBadge)
|
|
|
2715
|
+ let heroTitle = textLabel("Elevate your meetings workflow", font: NSFont.systemFont(ofSize: 22, weight: .bold), color: palette.textPrimary)
|
|
|
2716
|
+ let heroSubtitle = textLabel("Unlock automation, premium meeting tools, and priority support for every session.", font: NSFont.systemFont(ofSize: 13, weight: .medium), color: palette.textSecondary)
|
|
|
2717
|
+ heroTitle.maximumNumberOfLines = 2
|
|
|
2718
|
+ heroSubtitle.maximumNumberOfLines = 3
|
|
|
2719
|
+ hero.addSubview(heroTitle)
|
|
|
2720
|
+ hero.addSubview(heroSubtitle)
|
|
|
2721
|
+ NSLayoutConstraint.activate([
|
|
|
2722
|
+ heroBadge.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
|
|
|
2723
|
+ heroBadge.topAnchor.constraint(equalTo: hero.topAnchor, constant: 14),
|
|
|
2724
|
+ heroTitle.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
|
|
|
2725
|
+ heroTitle.trailingAnchor.constraint(lessThanOrEqualTo: hero.trailingAnchor, constant: -16),
|
|
|
2726
|
+ heroTitle.topAnchor.constraint(equalTo: heroBadge.bottomAnchor, constant: 10),
|
|
|
2727
|
+ heroSubtitle.leadingAnchor.constraint(equalTo: heroTitle.leadingAnchor),
|
|
|
2728
|
+ heroSubtitle.trailingAnchor.constraint(equalTo: hero.trailingAnchor, constant: -16),
|
|
|
2729
|
+ heroSubtitle.topAnchor.constraint(equalTo: heroTitle.bottomAnchor, constant: 8),
|
|
|
2730
|
+ heroSubtitle.bottomAnchor.constraint(equalTo: hero.bottomAnchor, constant: -16)
|
|
|
2731
|
+ ])
|
|
2608
|
2732
|
|
|
2609
|
|
- contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textSecondary))
|
|
2610
|
|
- let benefits = paywallBenefitsSection()
|
|
2611
|
|
- contentStack.addArrangedSubview(benefits)
|
|
2612
|
|
- contentStack.setCustomSpacing(18, after: benefits)
|
|
|
2733
|
+ let benefitsRow = NSStackView()
|
|
|
2734
|
+ benefitsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2735
|
+ benefitsRow.orientation = .horizontal
|
|
|
2736
|
+ benefitsRow.spacing = 8
|
|
|
2737
|
+ benefitsRow.distribution = .fillEqually
|
|
|
2738
|
+ benefitsRow.alignment = .centerY
|
|
|
2739
|
+ benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage meetings"))
|
|
|
2740
|
+ benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🧠", text: "Smart scheduling"))
|
|
|
2741
|
+ benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Faster workflow"))
|
|
|
2742
|
+ benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🔔", text: "Reminder notifications"))
|
|
|
2743
|
+ contentStack.addArrangedSubview(benefitsRow)
|
|
|
2744
|
+ benefitsRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
2745
|
+
|
|
|
2746
|
+ let plansRow = NSStackView()
|
|
|
2747
|
+ plansRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2748
|
+ plansRow.orientation = .horizontal
|
|
|
2749
|
+ plansRow.spacing = 10
|
|
|
2750
|
+ plansRow.distribution = .fillEqually
|
|
|
2751
|
+ plansRow.alignment = .centerY
|
|
2613
|
2752
|
|
|
2614
|
2753
|
let weeklyCard = paywallPlanCard(
|
|
2615
|
2754
|
title: "Weekly",
|
|
2616
|
2755
|
price: "PKR 1,100.00",
|
|
2617
|
|
- badge: "Basic Deal",
|
|
|
2756
|
+ badge: "Basic",
|
|
2618
|
2757
|
badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
|
|
2619
|
2758
|
subtitle: nil,
|
|
2620
|
2759
|
plan: .weekly,
|
|
2621
|
2760
|
strikePrice: nil
|
|
2622
|
2761
|
)
|
|
2623
|
|
- contentStack.addArrangedSubview(weeklyCard)
|
|
2624
|
|
-
|
|
2625
|
2762
|
let monthlyCard = paywallPlanCard(
|
|
2626
|
2763
|
title: "Monthly",
|
|
2627
|
2764
|
price: "PKR 2,500.00",
|
|
2628
|
|
- badge: "Free Trial",
|
|
|
2765
|
+ badge: "Popular",
|
|
2629
|
2766
|
badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
|
|
2630
|
|
- subtitle: "625.00/week",
|
|
|
2767
|
+ subtitle: "3 days free trial",
|
|
2631
|
2768
|
plan: .monthly,
|
|
2632
|
2769
|
strikePrice: nil
|
|
2633
|
2770
|
)
|
|
2634
|
|
- contentStack.addArrangedSubview(monthlyCard)
|
|
2635
|
|
-
|
|
2636
|
2771
|
let yearlyCard = paywallPlanCard(
|
|
2637
|
2772
|
title: "Yearly",
|
|
2638
|
2773
|
price: "PKR 9,900.00",
|
|
2639
|
|
- badge: "Best Deal",
|
|
|
2774
|
+ badge: "Best Value",
|
|
2640
|
2775
|
badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
|
|
2641
|
2776
|
subtitle: "190.38/week",
|
|
2642
|
2777
|
plan: .yearly,
|
|
2643
|
2778
|
strikePrice: nil
|
|
2644
|
2779
|
)
|
|
2645
|
|
- contentStack.addArrangedSubview(yearlyCard)
|
|
2646
|
|
-
|
|
2647
|
2780
|
let lifetimeCard = paywallPlanCard(
|
|
2648
|
2781
|
title: "Lifetime",
|
|
2649
|
2782
|
price: "PKR 14,900.00",
|
|
|
@@ -2653,9 +2786,25 @@ private extension ViewController {
|
|
2653
|
2786
|
plan: .lifetime,
|
|
2654
|
2787
|
strikePrice: "PKR 29,800.00"
|
|
2655
|
2788
|
)
|
|
2656
|
|
- contentStack.addArrangedSubview(lifetimeCard)
|
|
|
2789
|
+ plansRow.addArrangedSubview(weeklyCard)
|
|
|
2790
|
+ plansRow.addArrangedSubview(monthlyCard)
|
|
|
2791
|
+ plansRow.addArrangedSubview(yearlyCard)
|
|
|
2792
|
+ plansRow.addArrangedSubview(lifetimeCard)
|
|
|
2793
|
+ contentStack.addArrangedSubview(plansRow)
|
|
|
2794
|
+ plansRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
2657
|
2795
|
updatePaywallPlanSelection()
|
|
2658
|
|
- contentStack.setCustomSpacing(20, after: lifetimeCard)
|
|
|
2796
|
+
|
|
|
2797
|
+ let trustRow = NSStackView()
|
|
|
2798
|
+ trustRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2799
|
+ trustRow.orientation = .horizontal
|
|
|
2800
|
+ trustRow.spacing = 8
|
|
|
2801
|
+ trustRow.distribution = .fillEqually
|
|
|
2802
|
+ trustRow.alignment = .centerY
|
|
|
2803
|
+ trustRow.addArrangedSubview(paywallMetaItem(title: "Cancel anytime", subtitle: "No lock-in"))
|
|
|
2804
|
+ trustRow.addArrangedSubview(paywallMetaItem(title: "Instant access", subtitle: "Unlock all tools"))
|
|
|
2805
|
+ trustRow.addArrangedSubview(paywallMetaItem(title: "Secure billing", subtitle: "Handled by Apple"))
|
|
|
2806
|
+ contentStack.addArrangedSubview(trustRow)
|
|
|
2807
|
+ trustRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
2659
|
2808
|
|
|
2660
|
2809
|
let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
|
|
2661
|
2810
|
offer.alignment = .center
|
|
|
@@ -2663,26 +2812,24 @@ private extension ViewController {
|
|
2663
|
2812
|
let offerWrap = NSView()
|
|
2664
|
2813
|
offerWrap.translatesAutoresizingMaskIntoConstraints = false
|
|
2665
|
2814
|
offerWrap.addSubview(offer)
|
|
|
2815
|
+ contentStack.addArrangedSubview(offerWrap)
|
|
|
2816
|
+ offerWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
2666
|
2817
|
NSLayoutConstraint.activate([
|
|
2667
|
|
- offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
|
|
2668
|
2818
|
offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
|
|
2669
|
|
- offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
|
|
|
2819
|
+ offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 4),
|
|
2670
|
2820
|
offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
|
|
2671
|
2821
|
])
|
|
2672
|
|
- contentStack.addArrangedSubview(offerWrap)
|
|
2673
|
|
- contentStack.setCustomSpacing(18, after: offerWrap)
|
|
2674
|
2822
|
|
|
2675
|
2823
|
let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
|
|
2676
|
2824
|
continueButton.translatesAutoresizingMaskIntoConstraints = false
|
|
2677
|
2825
|
continueButton.isBordered = false
|
|
2678
|
2826
|
continueButton.bezelStyle = .regularSquare
|
|
2679
|
2827
|
continueButton.wantsLayer = true
|
|
2680
|
|
- continueButton.layer?.cornerRadius = 14
|
|
|
2828
|
+ continueButton.layer?.cornerRadius = 12
|
|
2681
|
2829
|
continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
|
|
2682
|
|
- continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
2683
|
|
- continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
2830
|
+ continueButton.heightAnchor.constraint(equalToConstant: 36).isActive = true
|
|
2684
|
2831
|
styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
|
|
2685
|
|
- let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
|
|
|
2832
|
+ let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 14, weight: .bold), color: .white)
|
|
2686
|
2833
|
continueButton.addSubview(continueLabel)
|
|
2687
|
2834
|
NSLayoutConstraint.activate([
|
|
2688
|
2835
|
continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
|
|
|
@@ -2698,31 +2845,36 @@ private extension ViewController {
|
|
2698
|
2845
|
paywallContinueButton = continueButton
|
|
2699
|
2846
|
paywallContinueLabel = continueLabel
|
|
2700
|
2847
|
contentStack.addArrangedSubview(continueButton)
|
|
2701
|
|
- contentStack.setCustomSpacing(16, after: continueButton)
|
|
|
2848
|
+ continueButton.widthAnchor.constraint(equalToConstant: 360).isActive = true
|
|
2702
|
2849
|
|
|
2703
|
2850
|
let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
|
|
2704
|
2851
|
secure.alignment = .center
|
|
2705
|
2852
|
let secureWrap = NSView()
|
|
2706
|
2853
|
secureWrap.translatesAutoresizingMaskIntoConstraints = false
|
|
2707
|
2854
|
secureWrap.addSubview(secure)
|
|
|
2855
|
+ contentStack.addArrangedSubview(secureWrap)
|
|
|
2856
|
+ secureWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
2708
|
2857
|
NSLayoutConstraint.activate([
|
|
2709
|
|
- secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
|
|
2710
|
2858
|
secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
|
|
2711
|
|
- secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
|
|
2712
|
|
- secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
|
|
|
2859
|
+ secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 2),
|
|
|
2860
|
+ secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -4)
|
|
2713
|
2861
|
])
|
|
2714
|
|
- contentStack.addArrangedSubview(secureWrap)
|
|
2715
|
|
- contentStack.setCustomSpacing(16, after: secureWrap)
|
|
2716
|
2862
|
|
|
2717
|
2863
|
let footer = paywallFooterLinks()
|
|
2718
|
2864
|
contentStack.addArrangedSubview(footer)
|
|
|
2865
|
+ footer.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
2719
|
2866
|
|
|
2720
|
|
- NSLayoutConstraint.activate([
|
|
2721
|
|
- contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
|
|
2722
|
|
- contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
|
|
2723
|
|
- contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
|
|
2724
|
|
- contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12)
|
|
2725
|
|
- ])
|
|
|
2867
|
+ var panelConstraints: [NSLayoutConstraint] = [
|
|
|
2868
|
+ contentStack.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
|
|
|
2869
|
+ contentStack.widthAnchor.constraint(equalToConstant: paywallLayoutWidth),
|
|
|
2870
|
+ contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 80),
|
|
|
2871
|
+ contentStack.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -12)
|
|
|
2872
|
+ ]
|
|
|
2873
|
+ if let closeButton {
|
|
|
2874
|
+ panelConstraints.append(closeButton.topAnchor.constraint(equalTo: panel.topAnchor, constant: 30))
|
|
|
2875
|
+ panelConstraints.append(closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -30))
|
|
|
2876
|
+ }
|
|
|
2877
|
+ NSLayoutConstraint.activate(panelConstraints)
|
|
2726
|
2878
|
|
|
2727
|
2879
|
refreshPaywallStoreUI()
|
|
2728
|
2880
|
return panel
|
|
|
@@ -2743,8 +2895,8 @@ private extension ViewController {
|
|
2743
|
2895
|
wrapper.bezelStyle = .regularSquare
|
|
2744
|
2896
|
wrapper.wantsLayer = true
|
|
2745
|
2897
|
wrapper.layer?.backgroundColor = NSColor.clear.cgColor
|
|
2746
|
|
- wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
2747
|
|
- wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
|
|
|
2898
|
+ wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 160).isActive = true
|
|
|
2899
|
+ wrapper.heightAnchor.constraint(equalToConstant: 162).isActive = true
|
|
2748
|
2900
|
wrapper.tag = plan.rawValue
|
|
2749
|
2901
|
|
|
2750
|
2902
|
let card = HoverTrackingView()
|
|
|
@@ -2752,7 +2904,7 @@ private extension ViewController {
|
|
2752
|
2904
|
card.wantsLayer = true
|
|
2753
|
2905
|
card.layer?.cornerRadius = 16
|
|
2754
|
2906
|
card.layer?.backgroundColor = palette.sectionCard.cgColor
|
|
2755
|
|
- card.heightAnchor.constraint(equalToConstant: 82).isActive = true
|
|
|
2907
|
+ card.heightAnchor.constraint(equalToConstant: 150).isActive = true
|
|
2756
|
2908
|
wrapper.addSubview(card)
|
|
2757
|
2909
|
NSLayoutConstraint.activate([
|
|
2758
|
2910
|
card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
|
|
@@ -2788,14 +2940,15 @@ private extension ViewController {
|
|
2788
|
2940
|
paywallPriceLabels[plan] = priceLabel
|
|
2789
|
2941
|
|
|
2790
|
2942
|
NSLayoutConstraint.activate([
|
|
2791
|
|
- badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
2943
|
+ badgeWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
|
|
2792
|
2944
|
badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
|
|
2793
|
2945
|
|
|
2794
|
|
- titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
|
|
2946
|
+ titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
|
|
2795
|
2947
|
titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
|
|
2796
|
2948
|
|
|
2797
|
|
- priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
|
2798
|
|
- priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32)
|
|
|
2949
|
+ priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
|
|
2950
|
+ priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
|
|
|
2951
|
+ priceLabel.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
|
|
2799
|
2952
|
])
|
|
2800
|
2953
|
|
|
2801
|
2954
|
if let subtitle {
|
|
|
@@ -2803,8 +2956,9 @@ private extension ViewController {
|
|
2803
|
2956
|
card.addSubview(sub)
|
|
2804
|
2957
|
paywallSubtitleLabels[plan] = sub
|
|
2805
|
2958
|
NSLayoutConstraint.activate([
|
|
2806
|
|
- sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
|
|
2807
|
|
- sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0)
|
|
|
2959
|
+ sub.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
|
|
|
2960
|
+ sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 2),
|
|
|
2961
|
+ sub.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
|
|
2808
|
2962
|
])
|
|
2809
|
2963
|
}
|
|
2810
|
2964
|
|
|
|
@@ -2812,8 +2966,9 @@ private extension ViewController {
|
|
2812
|
2966
|
let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
|
|
2813
|
2967
|
card.addSubview(strike)
|
|
2814
|
2968
|
NSLayoutConstraint.activate([
|
|
2815
|
|
- strike.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
|
|
2816
|
|
- strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4)
|
|
|
2969
|
+ strike.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
|
|
|
2970
|
+ strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4),
|
|
|
2971
|
+ strike.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
|
|
2817
|
2972
|
])
|
|
2818
|
2973
|
}
|
|
2819
|
2974
|
|
|
|
@@ -2830,8 +2985,8 @@ private extension ViewController {
|
|
2830
|
2985
|
func paywallFooterLinks() -> NSView {
|
|
2831
|
2986
|
let wrap = NSView()
|
|
2832
|
2987
|
wrap.translatesAutoresizingMaskIntoConstraints = false
|
|
2833
|
|
- wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
2834
|
|
- wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
2988
|
+ wrap.heightAnchor.constraint(equalToConstant: 40).isActive = true
|
|
|
2989
|
+ paywallFooterActionByView.removeAll()
|
|
2835
|
2990
|
|
|
2836
|
2991
|
let row = NSStackView()
|
|
2837
|
2992
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -2841,9 +2996,15 @@ private extension ViewController {
|
|
2841
|
2996
|
row.spacing = 0
|
|
2842
|
2997
|
wrap.addSubview(row)
|
|
2843
|
2998
|
|
|
2844
|
|
- row.addArrangedSubview(footerLink("Privacy Policy"))
|
|
2845
|
|
- row.addArrangedSubview(footerLink("Support"))
|
|
2846
|
|
- row.addArrangedSubview(footerLink("Terms of Services"))
|
|
|
2999
|
+ if storeKitCoordinator.hasPremiumAccess {
|
|
|
3000
|
+ row.addArrangedSubview(footerLink("Manage Subscription", action: .manageSubscription))
|
|
|
3001
|
+ row.addArrangedSubview(footerLink("Restore Purchase", action: .restorePurchase))
|
|
|
3002
|
+ } else {
|
|
|
3003
|
+ row.addArrangedSubview(footerLink("Continue with free plan", action: .continueWithFreePlan))
|
|
|
3004
|
+ }
|
|
|
3005
|
+ row.addArrangedSubview(footerLink("Privacy Policy", action: .privacyPolicy))
|
|
|
3006
|
+ row.addArrangedSubview(footerLink("Support", action: .support))
|
|
|
3007
|
+ row.addArrangedSubview(footerLink("Terms of Services", action: .termsOfServices))
|
|
2847
|
3008
|
|
|
2848
|
3009
|
NSLayoutConstraint.activate([
|
|
2849
|
3010
|
row.leadingAnchor.constraint(equalTo: wrap.leadingAnchor),
|
|
|
@@ -2855,25 +3016,54 @@ private extension ViewController {
|
|
2855
|
3016
|
return wrap
|
|
2856
|
3017
|
}
|
|
2857
|
3018
|
|
|
2858
|
|
- func footerLink(_ title: String) -> NSView {
|
|
2859
|
|
- let container = HoverTrackingView()
|
|
2860
|
|
- container.translatesAutoresizingMaskIntoConstraints = false
|
|
2861
|
|
- let label = textLabel(title, font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
|
|
2862
|
|
- label.alignment = .center
|
|
2863
|
|
- container.addSubview(label)
|
|
|
3019
|
+ private func footerLink(_ title: String, action: PaywallFooterAction) -> NSView {
|
|
|
3020
|
+ let button = HoverButton(title: title, target: self, action: #selector(paywallFooterButtonPressed(_:)))
|
|
|
3021
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
3022
|
+ button.isBordered = false
|
|
|
3023
|
+ button.bezelStyle = .regularSquare
|
|
|
3024
|
+ button.focusRingType = .none
|
|
|
3025
|
+ button.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
3026
|
+ button.alignment = .center
|
|
|
3027
|
+ button.setButtonType(.momentaryChange)
|
|
|
3028
|
+ button.contentTintColor = palette.textSecondary
|
|
|
3029
|
+ paywallFooterActionByView[ObjectIdentifier(button)] = action
|
|
|
3030
|
+ button.onHoverChanged = { [weak self, weak button] hovering in
|
|
|
3031
|
+ guard let self, let button else { return }
|
|
|
3032
|
+ button.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
3033
|
+ }
|
|
|
3034
|
+ button.onHoverChanged?(false)
|
|
|
3035
|
+ return button
|
|
|
3036
|
+ }
|
|
2864
|
3037
|
|
|
|
3038
|
+ func paywallMetaItem(title: String, subtitle: String) -> NSView {
|
|
|
3039
|
+ let card = HoverTrackingView()
|
|
|
3040
|
+ card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
3041
|
+ card.wantsLayer = true
|
|
|
3042
|
+ card.layer?.cornerRadius = 10
|
|
|
3043
|
+ card.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
3044
|
+ card.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
|
3045
|
+ styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
3046
|
+
|
|
|
3047
|
+ let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 11, weight: .semibold), color: palette.textPrimary)
|
|
|
3048
|
+ let subtitleLabel = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .medium), color: palette.textSecondary)
|
|
|
3049
|
+ card.addSubview(titleLabel)
|
|
|
3050
|
+ card.addSubview(subtitleLabel)
|
|
2865
|
3051
|
NSLayoutConstraint.activate([
|
|
2866
|
|
- label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
2867
|
|
- label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
|
|
|
3052
|
+ titleLabel.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
3053
|
+ titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 7),
|
|
|
3054
|
+ subtitleLabel.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
3055
|
+ subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 1),
|
|
|
3056
|
+ subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -6)
|
|
2868
|
3057
|
])
|
|
2869
|
3058
|
|
|
2870
|
|
- let click = NSClickGestureRecognizer(target: self, action: #selector(paywallFooterLinkClicked(_:)))
|
|
2871
|
|
- container.addGestureRecognizer(click)
|
|
2872
|
|
- container.onHoverChanged = { hovering in
|
|
2873
|
|
- label.textColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
3059
|
+ let base = palette.inputBackground
|
|
|
3060
|
+ let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
3061
|
+ let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
|
3062
|
+ card.onHoverChanged = { [weak card] hovering in
|
|
|
3063
|
+ card?.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
2874
|
3064
|
}
|
|
2875
|
|
- container.onHoverChanged?(false)
|
|
2876
|
|
- return container
|
|
|
3065
|
+ card.onHoverChanged?(false)
|
|
|
3066
|
+ return card
|
|
2877
|
3067
|
}
|
|
2878
|
3068
|
|
|
2879
|
3069
|
func paywallBenefitsSection() -> NSView {
|