Remove license and update about window.
This commit is contained in:
@@ -9,12 +9,13 @@ import AppKit
|
||||
|
||||
// MARK: - Constants
|
||||
fileprivate enum AboutLinks {
|
||||
static let privacyLink = "https://cmdbar.app/#privacy-policy"
|
||||
static let purchaseLink = "https://cmdbar.app/purchase"
|
||||
static let website = "https://cmdbar.app"
|
||||
static let documentation = "https://cmdbar.app/documentation"
|
||||
static let privacy = "https://cmdbar.app/#privacy-policy"
|
||||
}
|
||||
|
||||
enum Strings {
|
||||
static let copyright = "Copyright © 2024 GarikMI. All rights reserved."
|
||||
static let copyright = "Copyright © 2024\nGarikMI. All rights reserved."
|
||||
static let evaluationTitle = "License - Evaluation"
|
||||
static let evaluationMessage = "You are currently using evaluation license. CmdBar will quit after 20 minutes. If you already own a license, enter it below or purchase a license."
|
||||
static let activate = "Activate"
|
||||
@@ -30,7 +31,8 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
private var appIconImage: NSImageView = {
|
||||
//let image = NSImageView(image: NSApp.applicationIconImage)
|
||||
let image = NSImageView()
|
||||
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
|
||||
image.image = NSWorkspace.shared
|
||||
.icon(forFile: Bundle.main.bundlePath)
|
||||
image.imageScaling = .scaleAxesIndependently
|
||||
image.translatesAutoresizingMaskIntoConstraints = false
|
||||
return image
|
||||
@@ -38,12 +40,16 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
|
||||
private var appNameLabel: NSTextField = {
|
||||
let textField = NSTextField()
|
||||
textField.stringValue = (Bundle.main.infoDictionary?["CFBundleName"] as? String) ?? "NOT FOUND"
|
||||
textField.stringValue =
|
||||
(Bundle.main.infoDictionary?["CFBundleName"] as? String) ??
|
||||
"NOT FOUND"
|
||||
textField.isEditable = false
|
||||
textField.isBezeled = false
|
||||
textField.drawsBackground = false
|
||||
textField.alignment = .left
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold)
|
||||
textField.alignment = .center
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
|
||||
.preferredFontDescriptor(forTextStyle: .title1).pointSize,
|
||||
weight: .bold)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
@@ -54,9 +60,11 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
textField.isEditable = false
|
||||
textField.isBezeled = false
|
||||
textField.drawsBackground = false
|
||||
textField.alignment = .left
|
||||
textField.alignment = .center
|
||||
textField.textColor = NSColor.systemGray
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular)
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
|
||||
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
|
||||
weight: .regular)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
@@ -69,52 +77,11 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
textField.isEditable = false
|
||||
textField.isBezeled = false
|
||||
textField.drawsBackground = false
|
||||
textField.alignment = .left
|
||||
textField.alignment = .center
|
||||
textField.textColor = NSColor.systemGray
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var licenseLabel: NSTextField = {
|
||||
let textField = NSTextField()
|
||||
textField.stringValue = Strings.evaluationTitle
|
||||
textField.isEditable = false
|
||||
textField.isBezeled = false
|
||||
textField.drawsBackground = false
|
||||
textField.alignment = .left
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title2).pointSize, weight: .bold)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var licenseInfoLabel: NSTextField = {
|
||||
let textField = NSTextField()
|
||||
textField.stringValue = Strings.evaluationMessage
|
||||
textField.cell?.truncatesLastVisibleLine = true
|
||||
textField.maximumNumberOfLines = 6
|
||||
textField.isEditable = false
|
||||
textField.isBezeled = false
|
||||
textField.drawsBackground = false
|
||||
textField.alignment = .left
|
||||
textField.textColor = NSColor.systemGray
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var emailInput: EditableNSTextField = {
|
||||
let textField = EditableNSTextField()
|
||||
textField.placeholderString = "Email"
|
||||
textField.bezelStyle = .roundedBezel
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
|
||||
private var licenseKeyInput: EditableNSTextField = {
|
||||
let textField = EditableNSTextField()
|
||||
textField.placeholderString = "License Key"
|
||||
textField.bezelStyle = .roundedBezel
|
||||
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
|
||||
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
|
||||
weight: .regular)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
@@ -129,73 +96,46 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
return button
|
||||
}()
|
||||
|
||||
private var closeButton: NSButton = {
|
||||
private var documentationButton: NSButton = {
|
||||
let button = NSButton()
|
||||
button.title = "Close"
|
||||
button.title = "Docs"
|
||||
button.sizeToFit()
|
||||
button.bezelStyle = .rounded
|
||||
button.action = #selector(close)
|
||||
button.action = #selector(documentation)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
|
||||
private var purchaseButton: NSButton = {
|
||||
private var websiteButton: NSButton = {
|
||||
let button = NSButton()
|
||||
button.title = "Purchase"
|
||||
button.title = "CmdBar.app"
|
||||
button.sizeToFit()
|
||||
button.bezelStyle = .rounded
|
||||
button.action = #selector(purchase)
|
||||
button.action = #selector(website)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
|
||||
private var activateButton: NSButton = {
|
||||
let button = NSButton()
|
||||
button.isEnabled = false
|
||||
button.title = "Activate"
|
||||
button.keyEquivalent = "\r"
|
||||
button.sizeToFit()
|
||||
button.bezelStyle = .rounded
|
||||
button.action = #selector(activate)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
|
||||
private var dividerBox: NSBox = {
|
||||
let box = NSBox()
|
||||
box.boxType = .separator
|
||||
box.translatesAutoresizingMaskIntoConstraints = false
|
||||
return box
|
||||
private var buttonsContainer: NSLayoutGuide = {
|
||||
let container = NSLayoutGuide()
|
||||
return container
|
||||
}()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Input delegates
|
||||
emailInput.delegate = self
|
||||
licenseKeyInput.delegate = self
|
||||
|
||||
// Program info
|
||||
view.addSubview(appIconImage)
|
||||
view.addSubview(appNameLabel)
|
||||
view.addSubview(versionLabel)
|
||||
view.addSubview(copyrightLabel)
|
||||
|
||||
// Buttons
|
||||
view.addLayoutGuide(buttonsContainer)
|
||||
view.addSubview(privacyButton)
|
||||
|
||||
// Divider
|
||||
view.addSubview(dividerBox)
|
||||
|
||||
// License
|
||||
view.addSubview(licenseLabel)
|
||||
view.addSubview(licenseInfoLabel)
|
||||
view.addSubview(emailInput)
|
||||
view.addSubview(licenseKeyInput)
|
||||
|
||||
// Action buttons
|
||||
view.addSubview(closeButton)
|
||||
view.addSubview(purchaseButton)
|
||||
view.addSubview(activateButton)
|
||||
view.addSubview(documentationButton)
|
||||
view.addSubview(websiteButton)
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
@@ -203,7 +143,6 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
override func viewDidAppear() {
|
||||
super.viewDidAppear()
|
||||
self.view.window?.center()
|
||||
checkLicense()
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
@@ -214,155 +153,88 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
|
||||
private func setupConstraints() {
|
||||
// View.
|
||||
NSLayoutConstraint.activate([
|
||||
view.widthAnchor.constraint(equalToConstant: 600),
|
||||
view.widthAnchor.constraint(equalToConstant: 300),
|
||||
view.heightAnchor.constraint(lessThanOrEqualToConstant: 500),
|
||||
])
|
||||
|
||||
// App image.
|
||||
NSLayoutConstraint.activate([
|
||||
appIconImage.widthAnchor.constraint(equalToConstant: 60),
|
||||
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1),
|
||||
|
||||
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing20),
|
||||
appIconImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing20),
|
||||
appIconImage.widthAnchor.constraint(equalToConstant: 100),
|
||||
appIconImage.heightAnchor
|
||||
.constraint(equalTo: appIconImage.widthAnchor,
|
||||
multiplier: 1),
|
||||
appIconImage.topAnchor
|
||||
.constraint(equalTo: view.topAnchor,
|
||||
constant: ViewConstants.spacing20),
|
||||
appIconImage.centerXAnchor
|
||||
.constraint(equalTo: view.centerXAnchor),
|
||||
])
|
||||
|
||||
// Title
|
||||
NSLayoutConstraint.activate([
|
||||
appNameLabel.topAnchor.constraint(equalTo: appIconImage.topAnchor),
|
||||
appNameLabel.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10),
|
||||
appNameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
appNameLabel.topAnchor
|
||||
.constraint(equalTo: appIconImage.bottomAnchor,
|
||||
constant: ViewConstants.spacing20),
|
||||
appNameLabel.centerXAnchor
|
||||
.constraint(equalTo: view.centerXAnchor),
|
||||
|
||||
versionLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: ViewConstants.spacing2),
|
||||
versionLabel.leadingAnchor.constraint(equalTo: appNameLabel.leadingAnchor),
|
||||
versionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
versionLabel.topAnchor
|
||||
.constraint(equalTo: appNameLabel.bottomAnchor,
|
||||
constant: ViewConstants.spacing2),
|
||||
versionLabel.centerXAnchor
|
||||
.constraint(equalTo: view.centerXAnchor),
|
||||
|
||||
copyrightLabel.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: ViewConstants.spacing2),
|
||||
copyrightLabel.leadingAnchor.constraint(equalTo: appNameLabel.leadingAnchor),
|
||||
copyrightLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
|
||||
privacyButton.topAnchor.constraint(equalTo: copyrightLabel.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
privacyButton.leadingAnchor.constraint(equalTo: appNameLabel.leadingAnchor),
|
||||
copyrightLabel.topAnchor
|
||||
.constraint(equalTo: versionLabel.bottomAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
copyrightLabel.centerXAnchor
|
||||
.constraint(equalTo: view.centerXAnchor),
|
||||
])
|
||||
|
||||
// Buttons
|
||||
NSLayoutConstraint.activate([
|
||||
dividerBox.topAnchor.constraint(equalTo: privacyButton.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
dividerBox.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing20),
|
||||
dividerBox.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
])
|
||||
buttonsContainer.topAnchor
|
||||
.constraint(equalTo: copyrightLabel.bottomAnchor,
|
||||
constant: ViewConstants.spacing20),
|
||||
buttonsContainer.bottomAnchor
|
||||
.constraint(equalTo: view.bottomAnchor,
|
||||
constant: -ViewConstants.spacing20),
|
||||
buttonsContainer.centerXAnchor
|
||||
.constraint(equalTo: view.centerXAnchor),
|
||||
|
||||
// License labels.
|
||||
NSLayoutConstraint.activate([
|
||||
licenseLabel.topAnchor.constraint(equalTo: dividerBox.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
licenseLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing20),
|
||||
licenseLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
privacyButton.topAnchor
|
||||
.constraint(equalTo: buttonsContainer.topAnchor),
|
||||
privacyButton.bottomAnchor
|
||||
.constraint(equalTo: buttonsContainer.bottomAnchor),
|
||||
privacyButton.leadingAnchor
|
||||
.constraint(equalTo: buttonsContainer.leadingAnchor),
|
||||
|
||||
licenseInfoLabel.topAnchor.constraint(equalTo: licenseLabel.bottomAnchor, constant: ViewConstants.spacing2),
|
||||
licenseInfoLabel.leadingAnchor.constraint(equalTo: licenseLabel.leadingAnchor),
|
||||
licenseInfoLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
])
|
||||
documentationButton.firstBaselineAnchor
|
||||
.constraint(equalTo: privacyButton.firstBaselineAnchor),
|
||||
documentationButton.leadingAnchor
|
||||
.constraint(equalTo: privacyButton.trailingAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
|
||||
// License inputs.
|
||||
NSLayoutConstraint.activate([
|
||||
emailInput.topAnchor.constraint(equalTo: licenseInfoLabel.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
emailInput.leadingAnchor.constraint(equalTo: licenseInfoLabel.leadingAnchor),
|
||||
emailInput.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
|
||||
licenseKeyInput.topAnchor.constraint(equalTo: emailInput.bottomAnchor, constant: ViewConstants.spacing10),
|
||||
licenseKeyInput.leadingAnchor.constraint(equalTo: licenseInfoLabel.leadingAnchor),
|
||||
licenseKeyInput.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
])
|
||||
|
||||
// License buttons.
|
||||
NSLayoutConstraint.activate([
|
||||
activateButton.topAnchor.constraint(equalTo: licenseKeyInput.bottomAnchor, constant: ViewConstants.spacing20),
|
||||
activateButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
|
||||
activateButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing20),
|
||||
|
||||
purchaseButton.trailingAnchor.constraint(equalTo: activateButton.leadingAnchor, constant: -ViewConstants.spacing10),
|
||||
purchaseButton.lastBaselineAnchor.constraint(equalTo: activateButton.lastBaselineAnchor),
|
||||
|
||||
closeButton.trailingAnchor.constraint(equalTo: purchaseButton.leadingAnchor, constant: -ViewConstants.spacing10),
|
||||
closeButton.lastBaselineAnchor.constraint(equalTo: purchaseButton.lastBaselineAnchor),
|
||||
websiteButton.firstBaselineAnchor
|
||||
.constraint(equalTo: privacyButton.firstBaselineAnchor),
|
||||
websiteButton.leadingAnchor
|
||||
.constraint(equalTo: documentationButton.trailingAnchor,
|
||||
constant: ViewConstants.spacing10),
|
||||
websiteButton.trailingAnchor
|
||||
.constraint(equalTo: buttonsContainer.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Button Functions
|
||||
@objc private func privacy() {
|
||||
NSWorkspace.shared.open(URL(string: AboutLinks.privacyLink)!)
|
||||
NSWorkspace.shared.open(URL(string: AboutLinks.privacy)!)
|
||||
}
|
||||
|
||||
@objc private func close() {
|
||||
view.window?.orderOut(nil)
|
||||
@objc private func documentation() {
|
||||
NSWorkspace.shared.open(URL(string: AboutLinks.documentation)!)
|
||||
}
|
||||
|
||||
@objc private func purchase() {
|
||||
guard let url = URL(string: AboutLinks.purchaseLink) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
@objc private func activate() {
|
||||
let email = emailInput.stringValue
|
||||
let key = licenseKeyInput.stringValue
|
||||
if email.isEmpty || key.isEmpty { return }
|
||||
|
||||
activateButton.isEnabled = false
|
||||
activateButton.title = Strings.activating
|
||||
|
||||
// TODO: Notify the user if registration failed.
|
||||
LicenseManager.standard.activateLicense(email: email, key: key) { [weak self] _ in
|
||||
DispatchQueue.main.async { self?.checkLicense() }
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func deactivate() {
|
||||
LicenseManager.standard.deactivateLicense()
|
||||
checkLicense()
|
||||
}
|
||||
|
||||
private func checkLicense() {
|
||||
if LicenseManager.standard.isRegistered() {
|
||||
activateButton.isEnabled = true
|
||||
activateButton.title = Strings.deactivate
|
||||
activateButton.action = #selector(deactivate)
|
||||
|
||||
licenseLabel.stringValue = Strings.proTitle
|
||||
licenseInfoLabel.stringValue = Strings.proMessage
|
||||
|
||||
emailInput.isEnabled = false
|
||||
licenseKeyInput.isEnabled = false
|
||||
|
||||
let license = LicenseManager.standard.getLicense()
|
||||
emailInput.stringValue = license.email
|
||||
licenseKeyInput.stringValue =
|
||||
"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXX" +
|
||||
license.key.dropFirst(license.key.count - 5)
|
||||
} else {
|
||||
activateButton.isEnabled = false
|
||||
activateButton.title = Strings.activate
|
||||
activateButton.action = #selector(activate)
|
||||
|
||||
licenseLabel.stringValue = Strings.evaluationTitle
|
||||
licenseInfoLabel.stringValue = Strings.evaluationMessage
|
||||
|
||||
emailInput.isEnabled = true
|
||||
licenseKeyInput.isEnabled = true
|
||||
|
||||
emailInput.stringValue = ""
|
||||
licenseKeyInput.stringValue = ""
|
||||
|
||||
emailInput.becomeFirstResponder()
|
||||
}
|
||||
|
||||
#if(DEBUG)
|
||||
emailInput.stringValue = "igorkolokolnikov@icloud.com"
|
||||
licenseKeyInput.stringValue = "137A5010-A95A-4A8C-A037-6DF16B6CA666"
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension AboutViewController {
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
activateButton.isEnabled = !(emailInput.stringValue.isEmpty || licenseKeyInput.stringValue.isEmpty)
|
||||
@objc private func website() {
|
||||
NSWorkspace.shared.open(URL(string: AboutLinks.website)!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,28 +11,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
setupAboutWindow()
|
||||
setupUpdateWindow()
|
||||
setupNotifications()
|
||||
|
||||
setupLicense()
|
||||
|
||||
CmdManager.standard.configure()
|
||||
}
|
||||
|
||||
private func setupLicense() {
|
||||
let licenseManager = LicenseManager.standard
|
||||
if !licenseManager.isRegistered() {
|
||||
showAbout()
|
||||
licenseManager.clearLicense()
|
||||
licenseManager.startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUpdateWindow() {
|
||||
let controller = UpdateViewController()
|
||||
controller.setUpdateDelegate()
|
||||
|
||||
updateWindow = MenulessWindow(viewController: controller)
|
||||
updateWindow.title = "Software Update"
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
persistMenuBar(false)
|
||||
@@ -72,6 +53,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
// MARK: - Setup About Window
|
||||
private func setupAboutWindow() {
|
||||
aboutWindow = MenulessWindow(viewController: AboutViewController())
|
||||
aboutWindow.level = .statusBar
|
||||
}
|
||||
|
||||
private func setupUpdateWindow() {
|
||||
let controller = UpdateViewController()
|
||||
controller.setUpdateDelegate()
|
||||
|
||||
updateWindow = MenulessWindow(viewController: controller)
|
||||
updateWindow.title = "Software Update"
|
||||
updateWindow.level = .statusBar
|
||||
}
|
||||
|
||||
// MARK: - Show About Window
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
//
|
||||
// LicenseManager.swift
|
||||
// CmdBar
|
||||
//
|
||||
// Created by Igor Kolokolnikov on 8/26/23.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import CryptoKit
|
||||
|
||||
fileprivate struct LicenseModel: Codable {
|
||||
let valid: Bool
|
||||
let error: String
|
||||
let email: String
|
||||
let key: String
|
||||
}
|
||||
|
||||
// Converts string to a hash.
|
||||
fileprivate func hashString(_ string: String) -> String {
|
||||
return SHA256.hash(data: Data(string.utf8)).compactMap { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
// Converts passphrase to a SymmetricKey.
|
||||
fileprivate func passToKey(from passphrase: String) -> SymmetricKey {
|
||||
let passphraseData = Data(passphrase.utf8)
|
||||
let keyData = SHA256.hash(data: passphraseData).withUnsafeBytes { buffer in
|
||||
return Data(buffer)
|
||||
}
|
||||
return SymmetricKey(data: keyData)
|
||||
}
|
||||
|
||||
final class LicenseManager {
|
||||
static let standard = LicenseManager()
|
||||
|
||||
private let passkey = passToKey(from: "B2B67FB0E22B814CE5A6E7DA729A068BF21C9490919EA0B42FCEB322F9C9BA82")
|
||||
private let defaults = UserDefaults.standard
|
||||
private var timer: DispatchSourceTimer?
|
||||
private var quitAfterMinutes = 20.0 * 60.0 // quit after 20 minutes
|
||||
|
||||
func isRegistered() -> Bool {
|
||||
guard let email = defaults.object(forKey: "Email") as? String,
|
||||
let key = defaults.object(forKey: "Key") as? String,
|
||||
let licenseData = defaults.object(forKey: "License") as? Data,
|
||||
let unsealedBox = try? AES.GCM.SealedBox(combined: licenseData),
|
||||
let licenseHashData = try? AES.GCM.open(unsealedBox, using: passkey),
|
||||
!email.isEmpty,
|
||||
!key.isEmpty
|
||||
else { return false }
|
||||
|
||||
let currentHashData = Data(SHA256.hash(data: Data(hashString("\(email)\(key)\(NSUserName())").utf8))
|
||||
.compactMap { String(format: "%02x", $0) }.joined().utf8)
|
||||
|
||||
return licenseHashData == currentHashData
|
||||
}
|
||||
|
||||
func activateLicense(email: String, key: String, completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void) {
|
||||
guard let url = URL(string: EndpointConstants.validateURL)?.appending(component: key)
|
||||
else { completionHandler(.failure); return }
|
||||
|
||||
URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in
|
||||
guard
|
||||
let data = data,
|
||||
error == nil,
|
||||
let response = response as? HTTPURLResponse,
|
||||
(200...299).contains(response.statusCode)
|
||||
else { completionHandler(.failure); return }
|
||||
|
||||
guard let jsonResponse = try? JSONDecoder().decode(LicenseModel.self, from: data)
|
||||
else { completionHandler(.failure); return }
|
||||
|
||||
if jsonResponse.email == email && jsonResponse.key == key {
|
||||
self?.saveLicense(with: email, and: key)
|
||||
self?.stopTimer()
|
||||
completionHandler(.success)
|
||||
} else {
|
||||
completionHandler(.failure)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func deactivateLicense() {
|
||||
clearLicense()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
func getLicense() -> (email: String, key: String) {
|
||||
guard
|
||||
let email = defaults.object(forKey: "Email") as? String,
|
||||
let key = defaults.object(forKey: "Key") as? String
|
||||
else { return ("", "") }
|
||||
return (email, key)
|
||||
}
|
||||
|
||||
private func saveLicense(with email: String, and key: String) {
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
let licenseHashData = Data(SHA256.hash(data: Data(hashString("\(email)\(key)\(NSUserName())").utf8))
|
||||
.compactMap { String(format: "%02x", $0) }.joined().utf8)
|
||||
|
||||
guard let sealedBoxData = try? AES.GCM.seal(licenseHashData, using: passkey).combined else { return }
|
||||
defaults.set(email, forKey: "Email")
|
||||
defaults.set(key, forKey: "Key")
|
||||
defaults.set(sealedBoxData, forKey: "License")
|
||||
}
|
||||
|
||||
func clearLicense() {
|
||||
defaults.set(nil, forKey: "Email")
|
||||
defaults.set(nil, forKey: "Key")
|
||||
defaults.set(nil, forKey: "License")
|
||||
}
|
||||
|
||||
// MARK: - Timer
|
||||
func startTimer() {
|
||||
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
|
||||
timer?.schedule(
|
||||
deadline: .now() + quitAfterMinutes,
|
||||
repeating: DispatchTimeInterval.never
|
||||
)
|
||||
timer?.setEventHandler {
|
||||
DispatchQueue.main.async { NSApplication.shared.terminate(self) }
|
||||
}
|
||||
timer?.resume()
|
||||
}
|
||||
|
||||
func stopTimer() {
|
||||
timer?.cancel()
|
||||
timer = nil
|
||||
}
|
||||
}
|
||||
12
src/Makefile
12
src/Makefile
@@ -7,12 +7,12 @@ XCODE_PATH = $(shell xcode-select --print-path)
|
||||
|
||||
EXEC = CmdBar
|
||||
|
||||
SRCMODULES = UpdateManager.swift LicenseManager.swift \
|
||||
AboutViewController.swift AppDelegate.swift CBMenuBarItem.swift \
|
||||
CircularProgressView.swift CmdManager.swift CmdViewController.swift \
|
||||
CopyableTextView.swift EditableNSTextField.swift EventMonitor.swift \
|
||||
Helpers.swift MenulessWindow.swift PopoverPanel.swift \
|
||||
QuietView.swift UpdateViewController.swift main.swift
|
||||
SRCMODULES = UpdateManager.swift AboutViewController.swift \
|
||||
AppDelegate.swift CBMenuBarItem.swift CircularProgressView.swift \
|
||||
CmdManager.swift CmdViewController.swift CopyableTextView.swift \
|
||||
EditableNSTextField.swift EventMonitor.swift Helpers.swift \
|
||||
MenulessWindow.swift PopoverPanel.swift QuietView.swift \
|
||||
UpdateViewController.swift main.swift
|
||||
ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o))
|
||||
X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o))
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AppKit
|
||||
import Carbon
|
||||
|
||||
class MenulessWindow: NSWindow {
|
||||
init(viewController: NSViewController) {
|
||||
@@ -18,14 +19,17 @@ class MenulessWindow: NSWindow {
|
||||
}
|
||||
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
let commandKey = NSEvent.ModifierFlags.command.rawValue
|
||||
let modifiers = event.modifierFlags.rawValue
|
||||
let key = event.keyCode
|
||||
|
||||
if event.type == NSEvent.EventType.keyDown {
|
||||
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey,
|
||||
event.charactersIgnoringModifiers! == "w" {
|
||||
if modsContains(keys: OSCmd, in: modifiers) &&
|
||||
key == kVK_ANSI_W
|
||||
{
|
||||
performClose(nil)
|
||||
}
|
||||
}
|
||||
|
||||
return super.performKeyEquivalent(with: event)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user