Files
shotscreen/ShotScreen/Sources/LicenseEntryView.swift
Nick Roodenrijs 0dabed11d2 🎉 ShotScreen v1.0 - Initial Release
🚀 First official release of ShotScreen with complete feature set:

 Core Features:
- Advanced screenshot capture system
- Multi-monitor support
- Professional UI/UX design
- Automated update system with Sparkle
- Apple notarized & code signed

🛠 Technical Excellence:
- Native Swift macOS application
- Professional build & deployment pipeline
- Comprehensive error handling
- Memory optimized performance

📦 Distribution Ready:
- Professional DMG packaging
- Apple notarization complete
- No security warnings for users
- Ready for public distribution

This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
2025-06-28 16:15:15 +02:00

473 lines
19 KiB
Swift

import SwiftUI
import AppKit
struct LicenseEntryView: View {
@ObservedObject private var licenseManager = LicenseManager.shared
@Environment(\.presentationMode) var presentationMode
@State private var licenseKey = ""
@State private var userName = ""
@State private var isVerifying = false
@State private var errorMessage = ""
@State private var showError = false
// Add closure for window dismissal
var onDismiss: (() -> Void)?
init(onDismiss: (() -> Void)? = nil) {
self.onDismiss = onDismiss
}
var body: some View {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 24) {
// Add minimal top padding since traffic lights are hidden
Spacer()
.frame(height: 20)
VStack(spacing: 24) {
// Header Section - similar to SettingsTabView
VStack(spacing: 16) {
// Replace key icon with ShotScreen icon at native 200x200 resolution
Group {
if let shotScreenIcon = NSImage(named: "ShotScreenIcon_200x200") {
Image(nsImage: shotScreenIcon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.cornerRadius(20)
} else if let bundle = Bundle.main.url(forResource: "images/ShotScreenIcon_200x200", withExtension: "png"),
let nsImage = NSImage(contentsOf: bundle) {
Image(nsImage: nsImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.cornerRadius(20)
} else {
// Fallback to system icon if ShotScreenIcon_200x200 not found
Image(systemName: "key.fill")
.font(.system(size: 48))
.foregroundColor(.blue)
}
}
VStack(spacing: 8) {
Text(headerTitle)
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
Text(headerSubtitle)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
.padding(.top, 10)
.padding(.bottom, 10)
// Trial Status Section
switch licenseManager.licenseStatus {
case .trial(let daysLeft):
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("Trial Status")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.primary)
Divider()
}
HStack {
Image(systemName: "clock.fill")
.foregroundColor(.orange)
Text("\(daysLeft) days remaining in your free trial")
.foregroundColor(.secondary)
}
Text("Already purchased? Enter your license key below to activate the full version.")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(20)
.background(Color.primary.opacity(0.03))
.cornerRadius(12)
case .expired:
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("Trial Expired")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.red)
Divider()
}
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text("Your 7-day trial has expired")
.foregroundColor(.secondary)
}
Text("Purchase a license or enter your license key to continue using ShotScreen.")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(20)
.background(Color.red.opacity(0.05))
.cornerRadius(12)
default:
EmptyView()
}
// License Entry Form Section
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("License Information")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.primary)
Divider()
}
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("Your Name")
.font(.headline)
.foregroundColor(.primary)
TextField("Enter your full name", text: $userName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.disabled(isVerifying)
}
VStack(alignment: .leading, spacing: 8) {
Text("License Key")
.font(.headline)
.foregroundColor(.primary)
TextField("XXXX-XXXX-XXXX-XXXX", text: $licenseKey)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced))
.disabled(isVerifying)
}
if showError {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(errorMessage)
.foregroundColor(.red)
.font(.caption)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.red.opacity(0.1))
.cornerRadius(8)
}
}
}
.padding(20)
.background(Color.primary.opacity(0.03))
.cornerRadius(12)
Spacer()
}
.padding()
}
}
Divider()
// Bottom Button Section - matching SettingsTabView style
HStack {
Button(action: activateLicense) {
HStack {
if isVerifying {
ProgressView()
.scaleEffect(0.7)
}
Text(isVerifying ? "Verifying..." : "Activate License")
}
}
.buttonStyle(.borderedProminent)
.disabled(!canActivate || isVerifying)
.keyboardShortcut(.defaultAction) // Return key = Activate
Spacer()
Button("Buy License") {
openPurchaseURL()
}
.buttonStyle(.bordered)
Button("Cancel") {
dismissWindow()
}
.buttonStyle(.bordered)
}
.padding()
}
.frame(minWidth: 600, idealWidth: 650, minHeight: 700, idealHeight: 750)
.background(Color.clear)
}
private var canActivate: Bool {
!licenseKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
!userName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
private var headerTitle: String {
switch licenseManager.licenseStatus {
case .trial(_):
return "Activate ShotScreen"
case .expired:
return "Unlock ShotScreen"
default:
return "Activate ShotScreen"
}
}
private var headerSubtitle: String {
switch licenseManager.licenseStatus {
case .trial(_):
return "Already have a license? Enter your license key below."
case .expired:
return "Your free trial has ended. Enter your license key to continue using ShotScreen."
default:
return "Enter your license key to unlock the full version"
}
}
private func activateLicense() {
guard canActivate else { return }
isVerifying = true
showError = false
errorMessage = ""
Task {
let success = await licenseManager.enterLicenseKey(
licenseKey.trimmingCharacters(in: .whitespacesAndNewlines),
userName: userName.trimmingCharacters(in: .whitespacesAndNewlines)
)
await MainActor.run {
isVerifying = false
if success {
dismissWindow()
} else {
showError = true
errorMessage = "Invalid license key or verification failed. Please check your license key and internet connection."
}
}
}
}
private func openPurchaseURL() {
// ShotScreen Gumroad license product
if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") {
NSWorkspace.shared.open(url)
}
}
private func dismissWindow() {
onDismiss?()
presentationMode.wrappedValue.dismiss()
}
}
// MARK: - License Entry Window
class LicenseEntryWindow: NSWindow {
private var visualEffectViewContainer: NSView?
override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
setupWindow()
}
convenience init() {
self.init(
contentRect: NSRect(x: 0, y: 0, width: 650, height: 750),
styleMask: [.titled, .closable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
}
private func setupWindow() {
title = "ShotScreen License"
isReleasedWhenClosed = false
center()
// Ensure the window appears in front and can be brought to front
level = .normal
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
// Apply same blur style as SettingsWindow
isOpaque = false
backgroundColor = .clear
titlebarAppearsTransparent = true
titleVisibility = .hidden
isMovableByWindowBackground = true
// Hide traffic lights (red, yellow, green buttons) to prevent content overlap
standardWindowButton(.closeButton)?.isHidden = true
standardWindowButton(.miniaturizeButton)?.isHidden = true
standardWindowButton(.zoomButton)?.isHidden = true
// Create content view with dismissal callback
let licenseView = LicenseEntryView(onDismiss: { [weak self] in
self?.close()
})
let hostingView = NSHostingView(rootView: licenseView)
// Apply same visual effect as SettingsWindow
let visualEffectView = NSVisualEffectView()
visualEffectView.blendingMode = .behindWindow
visualEffectView.material = .hudWindow // Same strong system blur
visualEffectView.state = .active
visualEffectView.alphaValue = 1.0
visualEffectView.autoresizingMask = [.width, .height]
// Extra blur layer for enhanced effect
let extraBlurView = NSVisualEffectView()
extraBlurView.blendingMode = .behindWindow
extraBlurView.material = .hudWindow
extraBlurView.state = .active
extraBlurView.alphaValue = 0.6 // Half transparent for optical blur enhancement
extraBlurView.autoresizingMask = [.width, .height]
let newRootContentView = NSView(frame: contentRect(forFrameRect: frame))
// Layer order: strongest blur at bottom, half blur above, SwiftUI content on top
visualEffectView.frame = newRootContentView.bounds
extraBlurView.frame = newRootContentView.bounds
newRootContentView.addSubview(visualEffectView) // Layer 0
newRootContentView.addSubview(extraBlurView) // Layer 1 (enhancement)
newRootContentView.addSubview(hostingView) // Layer 2 (UI)
contentView = newRootContentView
visualEffectViewContainer = newRootContentView
hostingView.wantsLayer = true
hostingView.layer?.backgroundColor = NSColor.clear.cgColor
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.topAnchor.constraint(equalTo: newRootContentView.topAnchor),
hostingView.bottomAnchor.constraint(equalTo: newRootContentView.bottomAnchor),
hostingView.leadingAnchor.constraint(equalTo: newRootContentView.leadingAnchor),
hostingView.trailingAnchor.constraint(equalTo: newRootContentView.trailingAnchor)
])
// Force a layout pass after setting up the hierarchy
contentView?.layoutSubtreeIfNeeded()
// Window appearance
delegate = self
// Better window management - bring to front
orderFrontRegardless()
makeKeyAndOrderFront(nil)
}
deinit {
print("🗑️ LicenseEntryWindow deinitialized safely")
}
}
// MARK: - Window Delegate
extension LicenseEntryWindow: NSWindowDelegate {
func windowShouldClose(_ sender: NSWindow) -> Bool {
// Allow closing
return true
}
func windowWillClose(_ notification: Notification) {
// Clean up when window is about to close
contentView = nil
delegate = nil
print("🪟 License window closing gracefully")
}
}
// MARK: - Trial Expired View
struct TrialExpiredView: View {
@ObservedObject private var licenseManager = LicenseManager.shared
var body: some View {
VStack(spacing: 24) {
// Replace with ShotScreen icon using 200x200 source at smaller display size
Group {
if let shotScreenIcon = NSImage(named: "ShotScreenIcon_200x200") {
Image(nsImage: shotScreenIcon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
.cornerRadius(16)
} else if let bundle = Bundle.main.url(forResource: "images/ShotScreenIcon_200x200", withExtension: "png"),
let nsImage = NSImage(contentsOf: bundle) {
Image(nsImage: nsImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
.cornerRadius(16)
} else {
// Fallback icon
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 64))
.foregroundColor(.orange)
}
}
// Title
Text("Trial Expired")
.font(.largeTitle)
.fontWeight(.bold)
// Message
Text("Your 7-day trial of ShotScreen has expired.\nPlease purchase a license to continue using the app.")
.font(.body)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
// Buttons - matching new style
VStack(spacing: 12) {
Button("Purchase License") {
openPurchaseURL()
}
.buttonStyle(.borderedProminent)
Button("Enter License Key") {
licenseManager.showLicenseEntryDialog()
}
.buttonStyle(.bordered)
Button("Quit ShotScreen") {
NSApplication.shared.terminate(nil)
}
.buttonStyle(.bordered)
.foregroundColor(.red)
}
}
.padding(40)
.frame(width: 400, height: 350)
.background(Color(NSColor.windowBackgroundColor))
}
private func openPurchaseURL() {
// ShotScreen Gumroad license product
if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") {
NSWorkspace.shared.open(url)
}
}
}
#Preview {
LicenseEntryView()
}