diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index df78850..d1d0b46 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -28,6 +28,12 @@ jobs:
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
run: ./.github/scripts/signing.sh
+ - name: Update Build Number
+ env:
+ TAG_NAME: ${{ github.ref }}
+ run: |
+ export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/v//')
+ sed -i '' -e "s/CI_VERSION = 0.0.0/CI_VERSION = $CLEAN_TAG/g" Config/Config.xcconfig
- name: Build
run: xcrun xcodebuild -project Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIPs
diff --git a/Config/Config.xcconfig b/Config/Config.xcconfig
new file mode 100644
index 0000000..b265fbc
--- /dev/null
+++ b/Config/Config.xcconfig
@@ -0,0 +1 @@
+CI_VERSION = 0.0.0
\ No newline at end of file
diff --git a/SecretAgent/Info.plist b/SecretAgent/Info.plist
index 1716cbe..7ad766c 100644
--- a/SecretAgent/Info.plist
+++ b/SecretAgent/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- $(MARKETING_VERSION)
+ $(CI_VERSION)
CFBundleVersion
1
LSMinimumSystemVersion
diff --git a/SecretAgentKit/Info.plist b/SecretAgentKit/Info.plist
index d5ff04c..28ef30f 100644
--- a/SecretAgentKit/Info.plist
+++ b/SecretAgentKit/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.0
+ $(CI_VERSION)
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
diff --git a/SecretKit/Info.plist b/SecretKit/Info.plist
index d5ff04c..28ef30f 100644
--- a/SecretKit/Info.plist
+++ b/SecretKit/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.0
+ $(CI_VERSION)
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
diff --git a/Secretive.xcodeproj/project.pbxproj b/Secretive.xcodeproj/project.pbxproj
index ab19e88..b87464c 100644
--- a/Secretive.xcodeproj/project.pbxproj
+++ b/Secretive.xcodeproj/project.pbxproj
@@ -30,6 +30,9 @@
506838A12415EA5600F55094 /* AnySecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A02415EA5600F55094 /* AnySecret.swift */; };
506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A22415EA5D00F55094 /* AnySecretStore.swift */; };
506AB87E2412334700335D91 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 50731666241DF8660023809E /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50731665241DF8660023809E /* Updater.swift */; };
+ 50731669241E00C20023809E /* NoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50731668241E00C20023809E /* NoticeView.swift */; };
+ 508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */; };
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; };
5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02823FE35240062B6F2 /* SmartCardStore.swift */; };
@@ -179,6 +182,10 @@
5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = ""; };
506838A02415EA5600F55094 /* AnySecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecret.swift; sourceTree = ""; };
506838A22415EA5D00F55094 /* AnySecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretStore.swift; sourceTree = ""; };
+ 50731665241DF8660023809E /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; };
+ 50731668241E00C20023809E /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.swift; sourceTree = ""; };
+ 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewUpdater.swift; sourceTree = ""; };
+ 508A58AB241E121B0069DC07 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = ""; };
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = ""; };
5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = ""; };
@@ -274,6 +281,7 @@
50A3B78B24026B7500D209EA /* SecretAgent */,
5099A06D240242BA0062B6F2 /* SecretAgentKit */,
5099A07A240242BA0062B6F2 /* SecretAgentKitTests */,
+ 508A58AF241E144C0069DC07 /* Config */,
50617D8023FCE48E0099B055 /* Products */,
5099A08B240243730062B6F2 /* Frameworks */,
);
@@ -298,11 +306,13 @@
children = (
50617D8223FCE48E0099B055 /* AppDelegate.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */,
+ 50731668241E00C20023809E /* NoticeView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
50C385A8240B636500AF2719 /* SetupView.swift */,
+ 50731665241DF8660023809E /* Updater.swift */,
50617D8623FCE48E0099B055 /* Assets.xcassets */,
50617D8B23FCE48E0099B055 /* Main.storyboard */,
50617D8E23FCE48E0099B055 /* Info.plist */,
@@ -317,6 +327,7 @@
children = (
50617D8923FCE48E0099B055 /* Preview Assets.xcassets */,
50617DD123FCEFA90099B055 /* PreviewStore.swift */,
+ 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */,
);
path = "Preview Content";
sourceTree = "";
@@ -381,6 +392,14 @@
path = OpenSSH;
sourceTree = "";
};
+ 508A58AF241E144C0069DC07 /* Config */ = {
+ isa = PBXGroup;
+ children = (
+ 508A58AB241E121B0069DC07 /* Config.xcconfig */,
+ );
+ path = Config;
+ sourceTree = "";
+ };
5099A02523FE34DE0062B6F2 /* SmartCard */ = {
isa = PBXGroup;
children = (
@@ -735,9 +754,12 @@
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
+ 50731666241DF8660023809E /* Updater.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
+ 50731669241E00C20023809E /* NoticeView.swift in Sources */,
50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */,
+ 508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -871,6 +893,7 @@
/* Begin XCBuildConfiguration section */
50617D9B23FCE48E0099B055 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -931,6 +954,7 @@
};
50617D9C23FCE48E0099B055 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -991,6 +1015,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\"";
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_HARDENED_RUNTIME = YES;
@@ -1001,6 +1026,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1017,6 +1043,7 @@
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\"";
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_HARDENED_RUNTIME = YES;
@@ -1027,6 +1054,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Host";
@@ -1293,7 +1321,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
- MARKETING_VERSION = 0.1.0;
+ MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ -1318,7 +1346,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
- MARKETING_VERSION = 0.1.0;
+ MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent";
diff --git a/Secretive/AppDelegate.swift b/Secretive/AppDelegate.swift
index 300b805..2997b80 100644
--- a/Secretive/AppDelegate.swift
+++ b/Secretive/AppDelegate.swift
@@ -13,10 +13,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
list.add(store: SmartCard.Store())
return list
}()
+ let updater = PreviewUpdater()
func applicationDidFinishLaunching(_ aNotification: Notification) {
- let contentView = ContentView(storeList: storeList)
+ let contentView = ContentView(storeList: storeList, updater: updater)
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
diff --git a/Secretive/ContentView.swift b/Secretive/ContentView.swift
index bab54b9..c6e7489 100644
--- a/Secretive/ContentView.swift
+++ b/Secretive/ContentView.swift
@@ -1,38 +1,44 @@
import SwiftUI
import SecretKit
-struct ContentView: View {
+struct ContentView: View {
@ObservedObject var storeList: SecretStoreList
+ @ObservedObject var updater: UpdaterType
@State fileprivate var active: AnySecret.ID?
@State fileprivate var showingDeletion = false
@State fileprivate var deletingSecret: AnySecret?
var body: some View {
- NavigationView {
- List(selection: $active) {
- ForEach(storeList.stores) { store in
- if store.isAvailable {
- Section(header: Text(store.name)) {
- if store.secrets.isEmpty {
- if store is AnySecretStoreModifiable {
- NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) {
- Text("No Secrets")
+ VStack {
+ if updater.update != nil {
+ updateNotice()
+ }
+ NavigationView {
+ List(selection: $active) {
+ ForEach(storeList.stores) { store in
+ if store.isAvailable {
+ Section(header: Text(store.name)) {
+ if store.secrets.isEmpty {
+ if store is AnySecretStoreModifiable {
+ NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) {
+ Text("No Secrets")
+ }
+ } else {
+ NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) {
+ Text("No Secrets")
+ }
}
} else {
- NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) {
- Text("No Secrets")
- }
- }
- } else {
- ForEach(store.secrets) { secret in
- NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
- Text(secret.name)
- }.contextMenu {
- if store is AnySecretStoreModifiable {
- Button(action: { self.delete(secret: secret) }) {
- Text("Delete")
+ ForEach(store.secrets) { secret in
+ NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
+ Text(secret.name)
+ }.contextMenu {
+ if store is AnySecretStoreModifiable {
+ Button(action: { self.delete(secret: secret) }) {
+ Text("Delete")
+ }
}
}
}
@@ -40,31 +46,45 @@ struct ContentView: View {
}
}
}
+ }.onAppear {
+ let fallback: AnyHashable
+ if self.storeList.modifiableStore?.isAvailable ?? false {
+ fallback = Constants.emptyStoreModifiableTag
+ } else {
+ fallback = Constants.emptyStoreTag
+ }
+ self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
}
- }.onAppear {
- let fallback: AnyHashable
- if self.storeList.modifiableStore?.isAvailable ?? false {
- fallback = Constants.emptyStoreModifiableTag
- } else {
- fallback = Constants.emptyStoreTag
- }
- self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
+ .listStyle(SidebarListStyle())
+ .frame(minWidth: 100, idealWidth: 240)
}
- .listStyle(SidebarListStyle())
- .frame(minWidth: 100, idealWidth: 240)
- }
- .navigationViewStyle(DoubleColumnNavigationViewStyle())
- .sheet(isPresented: $showingDeletion) {
- if self.storeList.modifiableStore != nil {
- DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
- self.showingDeletion = false
+ .navigationViewStyle(DoubleColumnNavigationViewStyle())
+ .sheet(isPresented: $showingDeletion) {
+ if self.storeList.modifiableStore != nil {
+ DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
+ self.showingDeletion = false
+ }
}
}
}
-
}
-
-
+
+ func updateNotice() -> some View {
+ guard let update = updater.update else { return AnyView(Spacer()) }
+ let severity: NoticeView.Severity
+ let text: String
+ if update.critical {
+ severity = .critical
+ text = "Critical Security Update Required"
+ } else {
+ severity = .advisory
+ text = "Update Available"
+ }
+ return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update") {
+ NSWorkspace.shared.open(update.html_url)
+ })
+ }
+
func delete(secret: SecretType) {
deletingSecret = AnySecret(secret)
self.showingDeletion = true
@@ -72,24 +92,23 @@ struct ContentView: View {
}
-extension ContentView {
-
- enum Constants {
- static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
- static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
- }
-
+fileprivate enum Constants {
+ static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
+ static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
}
+
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
- ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
- ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
- ContentView(storeList: Preview.storeList(stores: [Preview.Store()]))
- ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]))
+ ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater())
+ ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater())
+ ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater())
+ ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater())
+ ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .advisory))
+ ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical))
}
}
}
diff --git a/Secretive/Info.plist b/Secretive/Info.plist
index e594e4f..14b20d7 100644
--- a/Secretive/Info.plist
+++ b/Secretive/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.0
+ $(CI_VERSION)
CFBundleVersion
- 1
+ $(CI_VERSION)
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
diff --git a/Secretive/NoticeView.swift b/Secretive/NoticeView.swift
new file mode 100644
index 0000000..dceba20
--- /dev/null
+++ b/Secretive/NoticeView.swift
@@ -0,0 +1,57 @@
+import Foundation
+import SwiftUI
+
+struct NoticeView: View {
+
+ let text: String
+ let severity: Severity
+ let actionTitle: String?
+ let action: (() -> Void)?
+
+ var body: some View {
+ HStack {
+ Text(text).bold()
+ Spacer()
+ if action != nil {
+ Button(action: action!) {
+ Text(actionTitle!)
+ }
+ }
+ }.padding().background(color)
+ }
+
+ var color: Color {
+ switch severity {
+ case .advisory:
+ return Color.orange
+ case .critical:
+ return Color.red
+ }
+ }
+
+}
+
+extension NoticeView {
+
+ enum Severity {
+ case advisory, critical
+ }
+
+}
+
+#if DEBUG
+
+struct NoticeView_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ NoticeView(text: "Agent Not Running", severity: .advisory, actionTitle: "Run Setup") {
+ print("OK")
+ }
+ NoticeView(text: "Critical Security Update Required", severity: .critical, actionTitle: "Update") {
+ print("OK")
+ }
+ }
+ }
+}
+
+#endif
diff --git a/Secretive/Preview Content/PreviewUpdater.swift b/Secretive/Preview Content/PreviewUpdater.swift
new file mode 100644
index 0000000..8cbeda8
--- /dev/null
+++ b/Secretive/Preview Content/PreviewUpdater.swift
@@ -0,0 +1,27 @@
+import Foundation
+import Combine
+
+class PreviewUpdater: UpdaterProtocol {
+
+ let update: Release?
+
+ init(update: Update = .none) {
+ switch update {
+ case .none:
+ self.update = nil
+ case .advisory:
+ self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Some regular update")
+ case .critical:
+ self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
+ }
+ }
+
+}
+
+extension PreviewUpdater {
+
+ enum Update {
+ case none, advisory, critical
+ }
+
+}
diff --git a/Secretive/Secretive.entitlements b/Secretive/Secretive.entitlements
index 82ba56c..8776520 100644
--- a/Secretive/Secretive.entitlements
+++ b/Secretive/Secretive.entitlements
@@ -6,6 +6,8 @@
com.apple.security.smartcard
+ com.apple.security.network.client
+
keychain-access-groups
$(AppIdentifierPrefix)com.maxgoedjen.Secretive
diff --git a/Secretive/Updater.swift b/Secretive/Updater.swift
new file mode 100644
index 0000000..25e361b
--- /dev/null
+++ b/Secretive/Updater.swift
@@ -0,0 +1,82 @@
+import Foundation
+import Combine
+
+protocol UpdaterProtocol: ObservableObject {
+
+ var update: Release? { get }
+
+}
+
+class Updater: ObservableObject, UpdaterProtocol {
+
+ @Published var update: Release?
+
+ init() {
+ checkForUpdates()
+ let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
+ self.checkForUpdates()
+ }
+ timer.tolerance = 60*60
+ }
+
+ func checkForUpdates() {
+ URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
+ guard let data = data else { return }
+ guard let release = try? JSONDecoder().decode(Release.self, from: data) else { return }
+ self.evaluate(release: release)
+ }.resume()
+ }
+
+ func evaluate(release: Release) {
+ let latestVersion = semVer(from: release.name)
+ let currentVersion = semVer(from: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
+ for (latest, current) in zip(latestVersion, currentVersion) {
+ if latest > current {
+ DispatchQueue.main.async {
+ self.update = release
+ }
+ return
+ }
+ }
+ }
+
+ func semVer(from stringVersion: String) -> [Int] {
+ var split = stringVersion.split(separator: ".").compactMap { Int($0) }
+ while split.count < 3 {
+ split.append(0)
+ }
+ return split
+ }
+
+}
+
+extension Updater {
+
+ enum Constants {
+ static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases/latest")!
+ }
+
+}
+
+struct Release: Codable {
+ let name: String
+ let html_url: URL
+ let body: String
+}
+
+
+extension Release {
+
+ var critical: Bool {
+ return body.contains(Constants.securityContent)
+ }
+
+}
+
+extension Release {
+
+ enum Constants {
+ static let securityContent = "Critical Security Update"
+ }
+
+}