mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Adds MacOS notifications
This commit is contained in:
parent
d43aac3346
commit
094bb6f0cc
7 changed files with 164 additions and 0 deletions
|
|
@ -99,7 +99,9 @@
|
|||
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
|
||||
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
|
||||
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
|
||||
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
|
||||
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
|
||||
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
|
||||
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
|
||||
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
|
@ -262,7 +264,9 @@
|
|||
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
|
||||
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
|
||||
E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = "<group>"; };
|
||||
E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
|
@ -425,6 +429,7 @@
|
|||
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
|
||||
CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */,
|
||||
CA42DD6D25AEA8B200BC0B0C /* Logger.swift */,
|
||||
E89342F925EDCC17007CF557 /* NotificationManager.swift */,
|
||||
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */,
|
||||
CABFA9AE2592EEE900380FEE /* Path+.swift */,
|
||||
CABFA9B42592EEEA00380FEE /* Process.swift */,
|
||||
|
|
@ -546,6 +551,7 @@
|
|||
CAFE4AAB25B7D2C70064FE51 /* GeneralPreferencePane.swift */,
|
||||
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */,
|
||||
E8977EA225C11E1500835F80 /* PreferencesView.swift */,
|
||||
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */,
|
||||
);
|
||||
path = Preferences;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -788,6 +794,7 @@
|
|||
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */,
|
||||
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
|
||||
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */,
|
||||
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */,
|
||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
|
||||
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
|
||||
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
|
||||
|
|
@ -814,6 +821,7 @@
|
|||
CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */,
|
||||
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
|
||||
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */,
|
||||
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */,
|
||||
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */,
|
||||
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */,
|
||||
CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ extension AppState {
|
|||
|
||||
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher<InstalledXcode, Error> {
|
||||
Logger.appState.info("Using \(downloader) downloader")
|
||||
|
||||
return getXcodeArchive(installationType, downloader: downloader)
|
||||
.flatMap { xcode, url -> AnyPublisher<InstalledXcode, Swift.Error> in
|
||||
self.installArchivedXcode(xcode, at: url)
|
||||
|
|
@ -442,6 +443,9 @@ extension AppState {
|
|||
DispatchQueue.main.async {
|
||||
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return }
|
||||
self.allXcodes[index].installState = .installing(step)
|
||||
|
||||
let xcode = self.allXcodes[index]
|
||||
Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ class AppState: ObservableObject {
|
|||
@Published var authenticationState: AuthenticationState = .unauthenticated
|
||||
@Published var availableXcodes: [AvailableXcode] = [] {
|
||||
willSet {
|
||||
if newValue.count > availableXcodes.count && availableXcodes.count != 0 {
|
||||
Current.notificationManager.scheduleNotification(title: "New Xcode versions", body: "New Xcode versions are available to download.", category: .normal)
|
||||
}
|
||||
updateAllXcodes(
|
||||
availableXcodes: newValue,
|
||||
installedXcodes: Current.files.installedXcodes(Path.root/"Applications"),
|
||||
|
|
@ -289,6 +292,7 @@ class AppState: ObservableObject {
|
|||
|
||||
func install(id: Xcode.ID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||
|
||||
installationPublishers[id] = signInIfNeeded()
|
||||
.flatMap { [unowned self] in
|
||||
// signInIfNeeded might finish before the user actually authenticates if UI is involved.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public struct Environment {
|
|||
public var defaults = Defaults()
|
||||
public var date: () -> Date = Date.init
|
||||
public var helper = Helper()
|
||||
public var notificationManager = NotificationManager()
|
||||
}
|
||||
|
||||
public var Current = Environment()
|
||||
|
|
@ -228,6 +229,11 @@ public struct Defaults {
|
|||
public func removeObject(forKey key: String) {
|
||||
removeObject(key)
|
||||
}
|
||||
|
||||
public var get: (String) -> Any? = { UserDefaults.standard.value(forKey: $0) }
|
||||
public func get(forKey key: String) -> Any? {
|
||||
get(key)
|
||||
}
|
||||
}
|
||||
|
||||
private let helperClient = HelperClient()
|
||||
|
|
|
|||
100
Xcodes/Backend/NotificationManager.swift
Normal file
100
Xcodes/Backend/NotificationManager.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import Foundation
|
||||
import os.log
|
||||
import UserNotifications
|
||||
|
||||
/// Representation of the 3 states of the Notifications permission prompt which may either have not been shown, or was shown and denied or accepted
|
||||
/// Unknown is value to indicate that we have not yet determined the status and should not be used other than as a default value before determining the actual status
|
||||
public enum NotificationPermissionPromptStatus: Int {
|
||||
case unknown, notShown, shownAndDenied, shownAndAccepted
|
||||
}
|
||||
|
||||
public enum XcodesNotificationCategory: String {
|
||||
case normal
|
||||
case error
|
||||
}
|
||||
|
||||
public enum XcodesNotificationType: String, Identifiable, CaseIterable, CustomStringConvertible {
|
||||
case newVersionAvailable
|
||||
case finishedInstalling
|
||||
|
||||
public var id: Self { self }
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .newVersionAvailable:
|
||||
return "New version is available"
|
||||
case .finishedInstalling:
|
||||
return "Finished Installing"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
private let notificationCenter = UNUserNotificationCenter.current()
|
||||
|
||||
@Published var notificationStatus = NotificationPermissionPromptStatus.unknown
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
loadNotificationStatus()
|
||||
|
||||
notificationCenter.delegate = self
|
||||
}
|
||||
|
||||
public func loadNotificationStatus() {
|
||||
UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { [weak self] (settings) in
|
||||
let status = NotificationManager.systemPromptStatusFromSettings(settings)
|
||||
self?.notificationStatus = status
|
||||
})
|
||||
}
|
||||
|
||||
private class func systemPromptStatusFromSettings(_ settings: UNNotificationSettings) -> NotificationPermissionPromptStatus {
|
||||
switch settings.authorizationStatus {
|
||||
case .notDetermined:
|
||||
return .notShown
|
||||
case .authorized, .provisional:
|
||||
return .shownAndAccepted
|
||||
case .denied:
|
||||
return .shownAndDenied
|
||||
@unknown default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
public func requestAccess() {
|
||||
|
||||
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
// Handle the error here.
|
||||
Logger.appState.error("Error requesting notification accesss: \(error.legibleLocalizedDescription)")
|
||||
} else {
|
||||
Logger.appState.log("User has \(granted ? "Granted" : "NOT GRANTED") notification permission")
|
||||
}
|
||||
self?.loadNotificationStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scheduleNotification(title: String?, body: String, category: XcodesNotificationCategory) {
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
if let title = title {
|
||||
content.title = title
|
||||
}
|
||||
content.body = body
|
||||
content.sound = .default
|
||||
content.categoryIdentifier = category.rawValue
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.3, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
|
||||
notificationCenter.add(request)
|
||||
}
|
||||
|
||||
// MARK: UNUserNotificationCenterDelegate
|
||||
|
||||
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler( [.banner, .badge, .sound])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +28,13 @@ struct GeneralPreferencePane: View {
|
|||
SignInCredentialsView(isPresented: $appState.presentingSignInAlert)
|
||||
.environmentObject(appState)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
GroupBox(label: Text("Notifications")) {
|
||||
NotificationsView().environmentObject(appState)
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
}
|
||||
.frame(width: 400)
|
||||
}
|
||||
|
|
|
|||
35
Xcodes/Frontend/Preferences/NotificationsView.swift
Normal file
35
Xcodes/Frontend/Preferences/NotificationsView.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import SwiftUI
|
||||
|
||||
struct NotificationsView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
switch Current.notificationManager.notificationStatus {
|
||||
case .shownAndAccepted:
|
||||
Text("Access Granted. You will receive notifications from Xcodes.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
case .shownAndDenied:
|
||||
Text("⚠️ Access Denied ⚠️\n\nPlease open your Notification Settings if you wish to allow access.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
default:
|
||||
Button("Enable Notifications", action: {
|
||||
Current.notificationManager.requestAccess()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct NotificationsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
NotificationsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue