Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add notifications for error status codes #101

Open
wants to merge 34 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6495c58
WIP: added middleware
martin-e91 May 27, 2021
6343afb
Added full url information
martin-e91 Jun 3, 2021
7dbfc20
AppServer setup
martin-e91 Jul 1, 2021
3120484
Mapping of failed responses to local notification
martin-e91 Jul 1, 2021
392086b
Merge branch 'develop' into feature/notification-upon-error
martin-e91 Jul 1, 2021
17b672e
Updated message
martin-e91 Jul 1, 2021
0cefa7b
Added Error details
martin-e91 Jul 1, 2021
c76a107
WIP: notifications settings
martin-e91 Jul 15, 2021
eb45b8c
Renamed middleware
martin-e91 Jul 15, 2021
0eb6dc9
Fixed server publisher
martin-e91 Jul 22, 2021
11f55a7
Added Notification section
martin-e91 Jul 22, 2021
1ed826c
Added notifications settings to user default
martin-e91 Jul 22, 2021
f646a15
Added notification enabling check
martin-e91 Jul 29, 2021
76bd8d1
removed import
martin-e91 Jul 29, 2021
3f0ddae
Clean
martin-e91 Jul 29, 2021
7f2c729
Refactored notification logic
martin-e91 Jul 29, 2021
2c310ed
Moved notifications subscription
martin-e91 Jul 29, 2021
25f35c5
Add doc
martin-e91 Jul 29, 2021
16cdce1
Clean
martin-e91 Jul 29, 2021
985a457
Format code
martin-e91 Jul 29, 2021
882f047
Added AppStorage to notifications permission
martin-e91 Jul 29, 2021
93420f3
Clean
martin-e91 Jul 29, 2021
ffa3d02
Merge branch 'feature/notification-upon-error' of github.com:wise-emo…
martin-e91 Jul 29, 2021
3dd14dc
Format code
martin-e91 Jul 29, 2021
ecc06f1
Removed abort error warning
martin-e91 Jul 29, 2021
c7d89cd
Merge branch 'feature/notification-upon-error' of github.com:wise-emo…
martin-e91 Jul 29, 2021
e0d3b90
Edited InAppNotification. Add UT
martin-e91 Jul 29, 2021
5346d2f
Format code
martin-e91 Jul 29, 2021
20457cf
Add UT
martin-e91 Jul 29, 2021
ff04bab
Merge branch 'feature/notification-upon-error' of github.com:wise-emo…
martin-e91 Jul 29, 2021
fbe355c
Format code
martin-e91 Jul 29, 2021
2712e62
updated notification logic
martin-e91 Jul 29, 2021
22801f5
Format code
martin-e91 Jul 29, 2021
81f82b1
Merge branch 'develop' into feature/notification-upon-error
FabrizioBrancati Sep 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions Sources/App/Environments/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@
// Mocka
//

import Foundation
import Combine
import MockaServer
import SwiftUI

/// App environment object shared by all the `View`s of the application.
final class AppEnvironment: ObservableObject {
/// Whether or not the in-app notifications are enabled.
@Published var areInAppNotificationEnabled: Bool = Logic.Settings.Notifications.areInAppNotificationEnabled {
didSet {
Logic.Settings.Notifications.areInAppNotificationEnabled = areInAppNotificationEnabled
}
}

/// The subscription for mapping the failed requests into `InAppNotifications`.
var failedRequestsNotificationSubscription: AnyCancellable?

/// Whether the server is currently running.
@Published var isServerRunning: Bool = false
@Published var isServerRunning: Bool = false {
didSet {
Logic.Settings.Notifications.updateFailedRequestsNotificationSubscription(in: self)
}
}

/// The selected app section, selected by using the app's Sidebar.
@Published var selectedSection: SidebarSection = .server
Expand Down
3 changes: 3 additions & 0 deletions Sources/App/Helpers/SFSymbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import Foundation

/// The San Francisco Symbol enum.
enum SFSymbol: String {
/// The bell icon.
case bell

/// Document icon.
case document = "doc.fill"

Expand Down
10 changes: 8 additions & 2 deletions Sources/App/Helpers/Typealiases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@

import Foundation

/// An `API` path.
typealias Path = [String]
/// A closure for reacting to a permission authorization request.
typealias AuthorizationRequestCompletion = (Bool, Error?) -> Void

/// A closure block with a parameter.
typealias CustomInteraction<T> = (T) -> Void

/// A closure with no parameters, and no output.
typealias Interaction = () -> Void

/// An `API` path.
typealias Path = [String]
100 changes: 100 additions & 0 deletions Sources/App/Logic/Notification+Logic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Mocka
//

import SwiftUI
import UserNotifications

extension Logic.Settings {
/// The logic related to notifications settings of the app.
enum Notifications {}
}

extension Logic.Settings.Notifications {
/// Whether or not the in-app notifications are enabled.
@AppStorage(UserDefaultKey.areInAppNotificationEnabled) static var areInAppNotificationEnabled: Bool = false

/// The `UNUserNotificationCenter` used for handling notifications.
private static var notificationCenter: UNUserNotificationCenter {
.current()
}

/// Builds an `UNNotificationRequest` with the given `notification`'s content and adds it to the notification center.
///
/// - Parameters:
/// - notification: The in app
/// - identifier: The identifier of the request that will be built.
/// - completion: The block to execute with the results. If the notification is successfully scheduled the returned error is `nil`.
///
/// - Note: This method does nothing when `areInAppNotificationEnabled` is `false`.
static func add(notification: InAppNotification, with identifier: String = UUID().uuidString, completion: CustomInteraction<Error?>? = nil) {
addNotificationRequest(with: notification.content, identifier: identifier, completion: completion)
}

/// Builds an `UNNotificationRequest` with the given `content` and adds it to the notification center.
///
/// - Parameters:
/// - content: The content of the request.
/// - identifier: The identifier of the request that will be built.
/// - completion: The block to execute with the results. If the notification is successfully scheduled the returned error is `nil`.
///
/// - Note: This method does nothing when `areInAppNotificationEnabled` is `false`.
static func addNotificationRequest(
with content: UNNotificationContent,
identifier: String = UUID().uuidString,
completion: CustomInteraction<Error?>? = nil
) {
guard areInAppNotificationEnabled else {
return
}

requestNotificationsAuthorizationIfNecessary { isPermissionGiven, error in
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
notificationCenter.add(request, withCompletionHandler: completion)
}
}

/// Requests notifications permissions if the user never answered to it.
///
/// - Parameter completion: The closure excecuted when the user answers the request.
static func requestNotificationsAuthorizationIfNecessary(_ completion: @escaping AuthorizationRequestCompletion) {
UNUserNotificationCenter.current()
.getNotificationSettings { settings in
switch settings.authorizationStatus {
case .notDetermined:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound], completionHandler: completion)

case .authorized, .provisional:
completion(true, nil)

case .denied:
completion(
false, NSError(domain: "App Permissions", code: 0, userInfo: [NSLocalizedDescriptionKey: "User denied notifications permissions"]))

@unknown default:
completion(false, NSError(domain: "App Permissions", code: 0, userInfo: [NSLocalizedDescriptionKey: "Unknown authorizationStatus value"]))
return
}
}
}

/// Updates the `failedRequestsNotificationSubscription` in the given `appEnvironment`.
///
/// - Parameter appEnvironment: The `AppEnvironment` instance to be updated.
static func updateFailedRequestsNotificationSubscription(in appEnvironment: AppEnvironment) {
guard appEnvironment.isServerRunning else {
appEnvironment.failedRequestsNotificationSubscription = nil
return
}

appEnvironment.failedRequestsNotificationSubscription = appEnvironment.server.networkExchangesPublisher
.receive(on: RunLoop.main)
.filter {
let statusCode = $0.response.status.code
return statusCode >= 300 && statusCode <= 600
}
.sink {
Logic.Settings.Notifications.add(notification: .failedResponse(statusCode: $0.response.status.code, path: $0.response.uri.path))
}
}
}
66 changes: 33 additions & 33 deletions Sources/App/Logic/Settings+Logic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,52 @@
// Mocka
//

import Foundation
import UniformTypeIdentifiers

extension Logic {
/// The logic related to settings of the app.
enum Settings {
/// The name of the file containing the server's configuration.
static let serverConfigurationFileName = "serverConfiguration.json"

/// Checks if the workspace `URL` saved in the `UserDefaults` is valid.
/// That is, it is not nil, it exists, and it contains the `serverConfiguration.json` file.
static var isWorkspaceURLValid: Bool {
guard let savedWorkspaceURL = UserDefaults.standard.url(forKey: UserDefaultKey.workspaceURL) else {
return false
}
enum Settings {}
}

return
FileManager.default.fileExists(atPath: savedWorkspaceURL.path)
&& uniformType(of: savedWorkspaceURL) == UTType.folder
&& serverConfigurationFileExists(at: savedWorkspaceURL)
extension Logic.Settings {
/// The name of the file containing the server's configuration.
static let serverConfigurationFileName = "serverConfiguration.json"

/// Checks if the workspace `URL` saved in the `UserDefaults` is valid.
/// That is, it is not nil, it exists, and it contains the `serverConfiguration.json` file.
static var isWorkspaceURLValid: Bool {
guard let savedWorkspaceURL = UserDefaults.standard.url(forKey: UserDefaultKey.workspaceURL) else {
return false
}

/// The server configuration.
///
/// This variable extracts the hostname and port of the server from the `serverConfigurationFileName`.
/// In addition, it tries to fetch all the requests from the workspace path.
/// Should either of the two steps fail, it returns nil.
static var serverConfiguration: ServerConfiguration? {
guard
let workspaceURL = UserDefaults.standard.url(forKey: UserDefaultKey.workspaceURL),
let settingsFileData = FileManager.default.contents(
atPath: workspaceURL.appendingPathComponent(serverConfigurationFileName, isDirectory: false).path
),
let serverConfiguration = try? JSONDecoder().decode(ServerConnectionConfiguration.self, from: settingsFileData),
let requests = try? Logic.SourceTree.requests()
else {
return nil
}
return
FileManager.default.fileExists(atPath: savedWorkspaceURL.path)
&& uniformType(of: savedWorkspaceURL) == UTType.folder
&& serverConfigurationFileExists(at: savedWorkspaceURL)
}

return ServerConfiguration(hostname: serverConfiguration.hostname, port: serverConfiguration.port, requests: requests)
/// The server configuration.
///
/// This variable extracts the hostname and port of the server from the `serverConfigurationFileName`.
/// In addition, it tries to fetch all the requests from the workspace path.
/// Should either of the two steps fail, it returns nil.
static var serverConfiguration: ServerConfiguration? {
guard
let workspaceURL = UserDefaults.standard.url(forKey: UserDefaultKey.workspaceURL),
let settingsFileData = FileManager.default.contents(
atPath: workspaceURL.appendingPathComponent(serverConfigurationFileName, isDirectory: false).path
),
let serverConfiguration = try? JSONDecoder().decode(ServerConnectionConfiguration.self, from: settingsFileData),
let requests = try? Logic.SourceTree.requests()
else {
return nil
}

return ServerConfiguration(hostname: serverConfiguration.hostname, port: serverConfiguration.port, requests: requests)
}
}

extension Logic.Settings {

/// Updates the server configuration file or creates it at the workspace root folder.
/// - Throws: `MockaError.workspacePathDoesNotExist`,
/// `MockaError.failedToEncode`.
Expand Down
1 change: 1 addition & 0 deletions Sources/App/Mocka.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct Mocka: App {

Settings {
AppSettings()
.environmentObject(appEnvironment)
}
}
}
26 changes: 26 additions & 0 deletions Sources/App/Models/InAppNotification.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// Mocka
//

import UserNotifications

/// An in-app notification to be showed to the user.
enum InAppNotification {
/// The in-app notification for when a request fails.
case failedResponse(statusCode: UInt, path: String)

/// The `UNNotificationContent` for the in-app notification.
var content: UNNotificationContent {
let content = UNMutableNotificationContent()

switch self {
case let .failedResponse(statusCode, path):
content.body = "Received response with \(statusCode) status code."
content.title = "Request failed!"
content.subtitle = "Endpoint: \(path)"
content.sound = .default
}

return content
}
}
3 changes: 3 additions & 0 deletions Sources/App/Models/UserDefaultKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import Foundation

/// The `Key`s used to save and fetch values from `UserDefaults`.
enum UserDefaultKey {
/// The key for retrieving the activation status of the in-app notifications.
static let areInAppNotificationEnabled = "areInAppNotificationEnabled"

/// The workspace path that should be used when fetching and saving the requests and responses created by the user.
static let workspaceURL = "workspaceURL"
}
10 changes: 5 additions & 5 deletions Sources/App/Views/Sections/Server/List/ServerListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class ServerListViewModel: ObservableObject {
/// The text that filters the requests.
@Published var filterText: String = ""

/// The array of `NetworkExchange`s.
/// The array of `NetworkExchange`s to be shown in the view.
@Published var networkExchanges: [NetworkExchange] = []

/// The `Set` containing the list of subscriptions.
Expand All @@ -39,11 +39,11 @@ final class ServerListViewModel: ObservableObject {

/// Creates a new instance with a `Publisher` of `NetworkExchange`s.
/// - Parameter networkExchangesPublisher: The publisher of `NetworkExchange`s.
init(networkExchangesPublisher: AnyPublisher<NetworkExchange, Never>) {
networkExchangesPublisher
init(networkExchangesPublisher: AnyPublisher<NetworkExchange, Never>?) {
networkExchangesPublisher?
.receive(on: RunLoop.main)
.sink { [weak self] in
self?.networkExchanges.append($0)
.sink { [weak self] networkExchange in
self?.networkExchanges.append(networkExchange)
}
.store(in: &subscriptions)
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/App/Views/Sections/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import SwiftUI

/// This is the main app settings `Settings`.
struct AppSettings: View {
/// The app environment object.
@EnvironmentObject var appEnvironment: AppEnvironment

// MARK: - Body

Expand All @@ -15,6 +17,13 @@ struct AppSettings: View {
.tabItem {
Label("Server", systemImage: SFSymbol.document.rawValue)
}

NotificationsSettings(viewModel: NotificationsSettingsViewModel(areInAppNotificationEnabled: appEnvironment.areInAppNotificationEnabled))
.tabItem {
Label("Notifications", systemImage: SFSymbol.bell.rawValue)
}
}
.padding(25)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .top)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Mocka
//

import SwiftUI

/// The notifications settings view.
struct NotificationsSettings: View {
/// The `ViewModel` for the view.
@State var viewModel: NotificationsSettingsViewModel

var body: some View {
VStack {
Toggle("Notifiche per request fallite", isOn: $viewModel.areInAppNotificationEnabled)
.toggleStyle(CheckboxToggleStyle())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Mocka
//

import SwiftUI

/// The `ViewModel` for the `NotificationsSettingsView`.
struct NotificationsSettingsViewModel {
/// Whether or not the in-app notifications are enabled.
var areInAppNotificationEnabled: Bool = Logic.Settings.Notifications.areInAppNotificationEnabled {
didSet {
Logic.Settings.Notifications.areInAppNotificationEnabled = areInAppNotificationEnabled
}
}
}
Loading