diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index b8f82bd..c17c9f2 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -25,11 +25,31 @@ jobs: build: name: CI/CD runs-on: macos-latest + permissions: + actions: read + contents: read + security-events: write steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Install tooling + run: | + brew install swiftlint + + - name: Generate SwiftLint SARIF + if: ${{ !cancelled() }} + run: | + swiftlint lint --no-cache --quiet --config GenericApp/.swiftlint.yml --reporter sarif GenericApp/GenericApp GenericApp/NotificationService > swiftlint.sarif + + - name: Upload SwiftLint SARIF + if: ${{ always() && !cancelled() && hashFiles('swiftlint.sarif') != '' }} + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + sarif_file: swiftlint.sarif + category: swiftlint + - name: Install App Store Connect API key env: APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }} @@ -72,4 +92,4 @@ jobs: # with: # name: ipas # path: | - # **/*.ipa \ No newline at end of file + # **/*.ipa diff --git a/GenericApp/.swiftlint.yml b/GenericApp/.swiftlint.yml new file mode 100644 index 0000000..6d0e157 --- /dev/null +++ b/GenericApp/.swiftlint.yml @@ -0,0 +1,4 @@ +disabled_rules: + - trailing_comma + - blanket_disable_command +line_length: 180 diff --git a/GenericApp/GenericApp.xcodeproj/project.pbxproj b/GenericApp/GenericApp.xcodeproj/project.pbxproj index 9c73f3d..6e096dd 100644 --- a/GenericApp/GenericApp.xcodeproj/project.pbxproj +++ b/GenericApp/GenericApp.xcodeproj/project.pbxproj @@ -196,6 +196,7 @@ isa = PBXNativeTarget; buildConfigurationList = 878D2D1D249A490400011484 /* Build configuration list for PBXNativeTarget "GenericApp" */; buildPhases = ( + 917687E22F8E939C00D977BD /* SwiftLint */, 878D2D05249A490300011484 /* Sources */, 878D2D06249A490300011484 /* Frameworks */, 878D2D07249A490300011484 /* Resources */, @@ -218,6 +219,7 @@ 878D2D20249A498900011484 /* Sources */, 878D2D21249A498900011484 /* Frameworks */, 878D2D22249A498900011484 /* Resources */, + 917687E32F8E94E100D977BD /* SwiftLint */, ); buildRules = ( ); @@ -293,6 +295,53 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 917687E22F8E939C00D977BD /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/GenericApp", + "$(SRCROOT)/.swiftlint.yml", + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\n\nif [ \"${CI}\" = \"true\" ] || [ \"${GITHUB_ACTIONS}\" = \"true\" ]; then\n echo \"Skipping Xcode SwiftLint phase in CI.\"\n exit 0\nfi\n\nif command -v swiftlint >/dev/null; then\n swiftlint lint --no-cache --config \"${SRCROOT}/.swiftlint.yml\" --reporter xcode \"${SRCROOT}/GenericApp\" || true\nelse\n echo \"warning: SwiftLint not installed. Run 'brew install swiftlint' to enable local linting.\"\nfi\n"; + showEnvVarsInLog = 0; + }; + 917687E32F8E94E100D977BD /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/NotificationService", + "$(SRCROOT)/.swiftlint.yml", + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\n\nif [ \"${CI}\" = \"true\" ] || [ \"${GITHUB_ACTIONS}\" = \"true\" ]; then\n echo \"Skipping Xcode SwiftLint phase in CI.\"\n exit 0\nfi\n\nif command -v swiftlint >/dev/null; then\n swiftlint lint --no-cache --config \"${SRCROOT}/.swiftlint.yml\" --reporter xcode \"${SRCROOT}/NotificationService\" || true\nelse\n echo \"warning: SwiftLint not installed. Run 'brew install swiftlint' to enable local linting.\"\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 878D2D05249A490300011484 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/GenericApp/GenericApp/AppDelegate.swift b/GenericApp/GenericApp/AppDelegate.swift index fc2e5c4..39d81e8 100644 --- a/GenericApp/GenericApp/AppDelegate.swift +++ b/GenericApp/GenericApp/AppDelegate.swift @@ -29,20 +29,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, URLSessionDelegate { var window: UIWindow? let gcmMessageIDKey = "gcm.message_id" - var geofenceProvider : GeofenceProvider? + var geofenceProvider: GeofenceProvider? var fcmToken: String? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { UIBarButtonItem.appearance().setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(named: "or_green") as Any], for: .normal) UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(named: "or_green") as Any] UIBarButtonItem.appearance().tintColor = UIColor(named: "or_green") - + IQKeyboardManager.shared.isEnabled = true UNUserNotificationCenter.current().delegate = self // if the app was launched because of geofencing - // Redirects NSLog calls to file if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { let fileName = "\(Date()).log" @@ -87,11 +86,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, URLSessionDelegate { } func applicationWillResignActive(_ application: UIApplication) { - } func applicationDidEnterBackground(_ application: UIApplication) { - } func applicationWillEnterForeground(_ application: UIApplication) { @@ -107,7 +104,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, URLSessionDelegate { Messaging.messaging().apnsToken = deviceToken } - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + func application(_ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if let action = userInfo[DefaultsKey.actionKey] as? String { if action == Actions.geofenceRefresh { if let controllerGeofenceProvider = (self.window?.topController as? ORViewcontroller)?.geofenceProvider { @@ -141,47 +140,55 @@ class AppDelegate: UIResponder, UIApplicationDelegate, URLSessionDelegate { completionHandler(UIBackgroundFetchResult.newData) } - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) { + func urlSession(_ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { completionHandler(.performDefaultHandling, nil) } } } -extension AppDelegate : UNUserNotificationCenterDelegate { +extension AppDelegate: UNUserNotificationCenterDelegate { - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { let userInfo = notification.request.content.userInfo - var notificationId : Int64? = nil + var notificationId: Int64? - if let notificationIdString = userInfo[ActionType.notificationId] as? String{ + if let notificationIdString = userInfo[ActionType.notificationId] as? String { notificationId = Int64(notificationIdString) } - if let notiId = notificationId, let defaults = UserDefaults(suiteName: DefaultsKey.groupEntitlement), let consoleId = defaults.string(forKey: GeofenceProvider.consoleIdKey) { + if let notiId = notificationId, + let defaults = UserDefaults(suiteName: DefaultsKey.groupEntitlement), + let consoleId = defaults.string(forKey: GeofenceProvider.consoleIdKey) { ORNotificationResource.sharedInstance.notificationDelivered(notificationId: notiId, targetId: consoleId) } completionHandler([.banner, .sound]) } - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - + // swiftlint:disable:next cyclomatic_complexity function_body_length + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo - var notificationId : Int64? = nil - var consoleId : String? + var notificationId: Int64? + var consoleId: String? var project: ProjectConfig? - if let notificationIdString = userInfo[ActionType.notificationId] as? String{ + if let notificationIdString = userInfo[ActionType.notificationId] as? String { notificationId = Int64(notificationIdString) } - + if let userDefaults = UserDefaults(suiteName: DefaultsKey.groupEntitlement) { consoleId = userDefaults.string(forKey: GeofenceProvider.consoleIdKey) // TODO: geofence provider should also be adapted to store "per project" - + let selectedProjectId = userDefaults.string(forKey: DefaultsKey.projectKey) if let projectsData = userDefaults.data(forKey: DefaultsKey.projectsConfigurationKey) { let projects = (try? JSONDecoder().decode([ProjectConfig].self, from: projectsData)) ?? [] - project = projects.first(where:{ $0.id == selectedProjectId }) + project = projects.first(where: { $0.id == selectedProjectId }) } } @@ -192,19 +199,19 @@ extension AppDelegate : UNUserNotificationCenterDelegate { if let urlTo = userInfo[ActionType.appUrl] as? String, !urlTo.isEmpty { var urlRequest: URL? if urlTo.hasPrefix("http") || urlTo.hasPrefix("https") { - urlRequest = URL(string:urlTo) + urlRequest = URL(string: urlTo) } else { if let url = project?.baseURL { urlRequest = URL(string: "\(url)/console/\(urlTo)") } } - if let url = urlRequest{ - if let InBrowser = userInfo[ActionType.openInBrowser] as? Bool, InBrowser { + if let url = urlRequest { + if let inBrowser = userInfo[ActionType.openInBrowser] as? Bool, inBrowser { NSLog("%@", " in browser: \(url)") UIApplication.shared.open(url) } else { NSLog("%@", " in app: \(url)") - (self.window?.topController as? ORViewcontroller)?.loadURL(url:url) + (self.window?.topController as? ORViewcontroller)?.loadURL(url: url) } } } @@ -213,50 +220,48 @@ extension AppDelegate : UNUserNotificationCenterDelegate { if let notiId = notificationId, let conId = consoleId { ORNotificationResource.sharedInstance.notificationAcknowledged(notificationId: notiId, targetId: conId, acknowledgement: response.actionIdentifier) } - default : + default: if let notiId = notificationId, let conId = consoleId { ORNotificationResource.sharedInstance.notificationAcknowledged(notificationId: notiId, targetId: conId, acknowledgement: response.actionIdentifier) } if let buttonsString = userInfo[DefaultsKey.buttonsKey] as? String { if let buttonsData = buttonsString.data(using: .utf8) { if let buttons = try? JSONDecoder().decode([ORPushNotificationButton].self, from: buttonsData) { - for button in buttons { - if button.title == response.actionIdentifier { - if let action = button.action { - var urlRequest: URL? - if action.url.hasPrefix("http") || action.url.hasPrefix("https") { - urlRequest = URL(string:action.url) - } else { - if let url = project?.baseURL { - urlRequest = URL(string: "\(url)/console/\(action.url)") - } + for button in buttons where button.title == response.actionIdentifier { + if let action = button.action { + var urlRequest: URL? + if action.url.hasPrefix("http") || action.url.hasPrefix("https") { + urlRequest = URL(string: action.url) + } else { + if let url = project?.baseURL { + urlRequest = URL(string: "\(url)/console/\(action.url)") } - if let url = urlRequest { - if action.silent { - let request = NSMutableURLRequest(url: url) - request.httpMethod = action.httpMethod ?? "GET" - if let body = action.data { - request.httpBody = body.data(using: .utf8) - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - } - let session = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue : nil) - let reqDataTask = session.dataTask(with: request as URLRequest, completionHandler:{ data, response, error in - if (error != nil) { - NSLog("error %@", (error! as NSError).localizedDescription) - } - }) - reqDataTask.resume() - } else if action.openInBrowser { - NSLog("%@", " in browser: \(url)") - UIApplication.shared.open(url) - } else { - NSLog("%@", " in app: \(url)") - (self.window?.topController as? ORViewcontroller)?.loadURL(url:url) + } + if let url = urlRequest { + if action.silent { + let request = NSMutableURLRequest(url: url) + request.httpMethod = action.httpMethod ?? "GET" + if let body = action.data { + request.httpBody = body.data(using: .utf8) + request.addValue("application/json", forHTTPHeaderField: "Content-Type") } + let session = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: nil) + let reqDataTask = session.dataTask(with: request as URLRequest, completionHandler: { _, _, error in + if error != nil { + NSLog("error %@", (error! as NSError).localizedDescription) + } + }) + reqDataTask.resume() + } else if action.openInBrowser { + NSLog("%@", " in browser: \(url)") + UIApplication.shared.open(url) + } else { + NSLog("%@", " in app: \(url)") + (self.window?.topController as? ORViewcontroller)?.loadURL(url: url) } } - break } + break } } } @@ -266,10 +271,10 @@ extension AppDelegate : UNUserNotificationCenterDelegate { } } -extension AppDelegate : MessagingDelegate { +extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) { print("Firebase registration token: \(fcmToken)") - if let defaults = UserDefaults(suiteName: DefaultsKey.groupEntitlement){ + if let defaults = UserDefaults(suiteName: DefaultsKey.groupEntitlement) { defaults.set(fcmToken, forKey: DefaultsKey.fcmTokenKey) defaults.synchronize() } diff --git a/GenericApp/GenericApp/UI/ProjectTableViewCell.swift b/GenericApp/GenericApp/UI/ProjectTableViewCell.swift index d2e020e..d6d2dd0 100644 --- a/GenericApp/GenericApp/UI/ProjectTableViewCell.swift +++ b/GenericApp/GenericApp/UI/ProjectTableViewCell.swift @@ -20,15 +20,14 @@ import UIKit import ORLib - class ProjectTableViewCell: UITableViewCell { - + @IBOutlet weak var domainLabel: UILabel! @IBOutlet weak var appLabel: UILabel! @IBOutlet weak var realmLabel: UILabel! var project: ProjectConfig? - + func setProject(_ project: ProjectConfig) { self.project = project domainLabel.text = project.domain diff --git a/GenericApp/GenericApp/UI/SettingsViewController.swift b/GenericApp/GenericApp/UI/SettingsViewController.swift index 50538e5..9ee1428 100644 --- a/GenericApp/GenericApp/UI/SettingsViewController.swift +++ b/GenericApp/GenericApp/UI/SettingsViewController.swift @@ -21,10 +21,10 @@ import UIKit import ORLib class SettingsViewController: UITableViewController { - + private var projects = [ProjectConfig]() private var selectedProjectId: String? - + override func viewDidLoad() { super.viewDidLoad() @@ -39,7 +39,7 @@ class SettingsViewController: UITableViewController { navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addTapped)) navigationItem.title = "Projects" } - + override func setEditing(_ editing: Bool, animated: Bool) { if editing { navigationItem.leftBarButtonItem = nil @@ -52,7 +52,7 @@ class SettingsViewController: UITableViewController { @objc func addTapped() { self.performSegue(withIdentifier: Segues.addProject, sender: self) } - + @objc func doneTapped() { self.dismiss(animated: true) } @@ -60,15 +60,16 @@ class SettingsViewController: UITableViewController { override func numberOfSections(in tableView: UITableView) -> Int { return 2 } - - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return section == 0 ? projects.count : 1 } - + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if indexPath.section == 0 { - let cell: ProjectTableViewCell = tableView.dequeueReusableCell(withIdentifier: "ProjectCell", for: indexPath) as! ProjectTableViewCell + guard let cell = tableView.dequeueReusableCell(withIdentifier: "ProjectCell", for: indexPath) as? ProjectTableViewCell else { + fatalError("Invalid table view cell") + } let project = projects[indexPath.row] cell.setProject(project) cell.accessoryType = project.id == selectedProjectId ? .checkmark : .none @@ -80,7 +81,7 @@ class SettingsViewController: UITableViewController { return cell } } - + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { var rowsToReload = [IndexPath]() if editingStyle == .delete { @@ -93,7 +94,7 @@ class SettingsViewController: UITableViewController { rowsToReload.append(IndexPath(row: 0, section: 0)) } } - + do { if let userDefaults = UserDefaults(suiteName: DefaultsKey.groupEntitlement) { let data = try JSONEncoder().encode(projects) @@ -108,11 +109,11 @@ class SettingsViewController: UITableViewController { tableView.reloadRows(at: rowsToReload, with: .none) } } - + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) - if let currentlySelectedProject = projects.first(where:{ $0.id == selectedProjectId }), + if let currentlySelectedProject = projects.first(where: { $0.id == selectedProjectId }), let cellIndex = projects.firstIndex(of: currentlySelectedProject), let previousCell = tableView.cellForRow(at: IndexPath(row: cellIndex, section: indexPath.section)) { previousCell.accessoryType = .none @@ -125,7 +126,7 @@ class SettingsViewController: UITableViewController { } doneTapped() } - + private func selectProject(id: String?) { selectedProjectId = id if let userDefaults = UserDefaults(suiteName: DefaultsKey.groupEntitlement) { diff --git a/GenericApp/GenericApp/UI/SplashViewController.swift b/GenericApp/GenericApp/UI/SplashViewController.swift index 6dcc9f0..a33b37d 100644 --- a/GenericApp/GenericApp/UI/SplashViewController.swift +++ b/GenericApp/GenericApp/UI/SplashViewController.swift @@ -24,13 +24,13 @@ class SplashViewController: UIViewController { var host: String? var project: ProjectConfig? - + var displaySettings = false - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - - if (displaySettings) { + + if displaySettings { self.performSegue(withIdentifier: Segues.goToSettingsView, sender: self) displaySettings = false return @@ -38,14 +38,14 @@ class SplashViewController: UIViewController { if let userDefaults = UserDefaults(suiteName: DefaultsKey.groupEntitlement), let projectsData = userDefaults.data(forKey: DefaultsKey.projectsConfigurationKey), let selectedProjectId = userDefaults.string(forKey: DefaultsKey.projectKey) { - + let projects = try? JSONDecoder().decode([ProjectConfig].self, from: projectsData) - + if let projects = projects { print("Known projects \(projects)") print("Selected project \(selectedProjectId)") - - if let selectedProject = projects.first(where: { $0.id == selectedProjectId } ) { + + if let selectedProject = projects.first(where: { $0.id == selectedProjectId }) { project = selectedProject self.performSegue(withIdentifier: Segues.goToWebView, sender: self) return @@ -57,10 +57,12 @@ class SplashViewController: UIViewController { override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == Segues.goToWebView { - let orViewController = segue.destination as! ORViewcontroller - + guard let orViewController = segue.destination as? ORViewcontroller else { + fatalError("Invalid state for segue") + } + if let project = project { - + // TODO: replace with proper URL creation orViewController.targetUrl = project.targetUrl diff --git a/GenericApp/GenericApp/UI/WizardAppViewController.swift b/GenericApp/GenericApp/UI/WizardAppViewController.swift index c83d5a5..eae2c27 100644 --- a/GenericApp/GenericApp/UI/WizardAppViewController.swift +++ b/GenericApp/GenericApp/UI/WizardAppViewController.swift @@ -32,7 +32,7 @@ class WizardAppViewController: UIViewController { @IBOutlet weak var appTextInput: ORTextInput! @IBOutlet weak var nextButton: ORRaisedButton! @IBOutlet weak var boxView: UIView! - + @IBOutlet weak var appsSelectionButton: UIButton! var dropDown = DropDown() @@ -43,7 +43,7 @@ class WizardAppViewController: UIViewController { nextButton.backgroundColor = orGreenColor nextButton.tintColor = UIColor.white - + boxView.layer.cornerRadius = 10 } @@ -54,16 +54,16 @@ class WizardAppViewController: UIViewController { appTextInput.textField.autocorrectionType = .no appTextInput.textField.autocapitalizationType = .none appTextInput.textField.returnKeyType = .next - + if let apps = apps { dropDown.anchorView = appsSelectionButton // The list of items to display. Can be changed dynamically dropDown.dataSource = apps - dropDown.selectionAction = { [weak self] (index, item) in + dropDown.selectionAction = { [weak self] (_, item) in self?.appsSelectionButton.setTitle(item, for: .normal) } - + appsSelectionButton.isHidden = false appTextInput.isHidden = true } else { @@ -75,22 +75,25 @@ class WizardAppViewController: UIViewController { @IBAction func selectApp(_ sender: AnyObject) { dropDown.show() } - + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == Segues.goToWizardRealmView { switch configManager!.state { case .selectRealm(_, _, let realms): - let realmViewController = segue.destination as! WizardRealmViewController + guard let realmViewController = segue.destination as? WizardRealmViewController else { + fatalError("Invalid state for segue") + } realmViewController.realms = realms realmViewController.configManager = self.configManager default: fatalError("Invalid state for segue") } } else if segue.identifier == Segues.goToWebView { - let orViewController = segue.destination as! ORViewcontroller - switch configManager!.state { case .complete(let project): + guard let orViewController = segue.destination as? ORViewcontroller else { + fatalError("Invalid state for segue") + } orViewController.targetUrl = project.targetUrl default: fatalError("Invalid state for segue") @@ -101,7 +104,7 @@ class WizardAppViewController: UIViewController { @IBAction func nextButtonpressed(_ sender: UIButton) { selectApp() } - + private func selectApp() { let selectedApp: String? if apps != nil { @@ -113,12 +116,9 @@ class WizardAppViewController: UIViewController { if let selectedApp = selectedApp { print("Selected app >\(selectedApp)<") _ = try? configManager!.setApp(app: selectedApp) - - - + // TODO: check state, can we go to some other screen ? - - + self.performSegue(withIdentifier: Segues.goToWizardRealmView, sender: self) } else { let alertView = UIAlertController(title: "Error", message: "Please \(apps != nil ? "select" : "enter") an application", preferredStyle: .alert) @@ -128,13 +128,13 @@ class WizardAppViewController: UIViewController { } } } - + extension WizardAppViewController: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if textField == appTextInput.textField { - if let s = appTextInput.textField.text { - appName = s.replacingCharacters(in: Range(range, in: s)!, with: string).trimmingCharacters(in: .whitespacesAndNewlines) + if let originalString = appTextInput.textField.text { + appName = originalString.replacingCharacters(in: Range(range, in: originalString)!, with: string).trimmingCharacters(in: .whitespacesAndNewlines) } } return true diff --git a/GenericApp/GenericApp/UI/WizardDomainViewController.swift b/GenericApp/GenericApp/UI/WizardDomainViewController.swift index d400e80..3500fd8 100644 --- a/GenericApp/GenericApp/UI/WizardDomainViewController.swift +++ b/GenericApp/GenericApp/UI/WizardDomainViewController.swift @@ -29,7 +29,7 @@ class WizardDomainViewController: UIViewController { @IBOutlet weak var domainTextInput: ORTextInput! @IBOutlet weak var nextButton: ORRaisedButton! @IBOutlet var boxView: UIView! - + override func viewDidLoad() { super.viewDidLoad() @@ -48,35 +48,52 @@ class WizardDomainViewController: UIViewController { domainTextInput.textField.autocapitalizationType = .none domainTextInput.textField.returnKeyType = .next } - + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == Segues.goToWizardAppView { - switch configManager!.state { - case .selectApp(_, let apps): - let appViewController = segue.destination as! WizardAppViewController - appViewController.apps = apps - appViewController.configManager = self.configManager - default: + prepareWizardAppViewSegue(segue) + } else if segue.identifier == Segues.goToWizardRealmView { + prepareWizardRealmViewSegue(segue) + } else if segue.identifier == Segues.goToWebView { + prepareWebViewSegue(segue) + } + } + + private func prepareWizardAppViewSegue(_ segue: UIStoryboardSegue) { + switch configManager!.state { + case .selectApp(_, let apps): + guard let appViewController = segue.destination as? WizardAppViewController else { fatalError("Invalid state for segue") } - } else if segue.identifier == Segues.goToWizardRealmView { - switch configManager!.state { - case .selectRealm(_, _, let realms): - let realmViewController = segue.destination as! WizardRealmViewController - realmViewController.realms = realms - realmViewController.configManager = self.configManager - default: + appViewController.apps = apps + appViewController.configManager = self.configManager + default: + fatalError("Invalid state for segue") + } + } + + private func prepareWizardRealmViewSegue(_ segue: UIStoryboardSegue) { + switch configManager!.state { + case .selectRealm(_, _, let realms): + guard let realmViewController = segue.destination as? WizardRealmViewController else { fatalError("Invalid state for segue") } - } else if segue.identifier == Segues.goToWebView { - let orViewController = segue.destination as! ORViewcontroller - - switch configManager!.state { - case .complete(let project): - orViewController.targetUrl = project.targetUrl - default: + realmViewController.realms = realms + realmViewController.configManager = self.configManager + default: + fatalError("Invalid state for segue") + } + } + + private func prepareWebViewSegue(_ segue: UIStoryboardSegue) { + switch configManager!.state { + case .complete(let project): + guard let orViewController = segue.destination as? ORViewcontroller else { fatalError("Invalid state for segue") } + orViewController.targetUrl = project.targetUrl + default: + fatalError("Invalid state for segue") } } @@ -91,8 +108,8 @@ extension WizardDomainViewController: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if textField == domainTextInput.textField { - if let s = domainTextInput.textField.text { - domainName = s.replacingCharacters(in: Range(range, in: s)!, with: string).trimmingCharacters(in: .whitespacesAndNewlines) + if let originalString = domainTextInput.textField.text { + domainName = originalString.replacingCharacters(in: Range(range, in: originalString)!, with: string).trimmingCharacters(in: .whitespacesAndNewlines) nextButton.isEnabled = !(domainName?.isEmpty ?? true) } else { nextButton.isEnabled = false diff --git a/GenericApp/GenericApp/UI/WizardRealmViewController.swift b/GenericApp/GenericApp/UI/WizardRealmViewController.swift index bde883f..1347f5d 100644 --- a/GenericApp/GenericApp/UI/WizardRealmViewController.swift +++ b/GenericApp/GenericApp/UI/WizardRealmViewController.swift @@ -21,19 +21,18 @@ import UIKit import ORLib import DropDown - class WizardRealmViewController: UIViewController { - + var configManager: ConfigManager? var realms: [String]? var realmName: String? - + @IBOutlet weak var realmTextInput: ORTextInput! @IBOutlet weak var nextButton: ORRaisedButton! @IBOutlet weak var boxView: UIView! - + @IBOutlet weak var realmsSelectionButton: UIButton! var dropDown = DropDown() @@ -55,16 +54,16 @@ class WizardRealmViewController: UIViewController { realmTextInput.textField.autocorrectionType = .no realmTextInput.textField.autocapitalizationType = .none realmTextInput.textField.returnKeyType = .next - + if let realms = realms { dropDown.anchorView = realmsSelectionButton // The list of items to display. Can be changed dynamically dropDown.dataSource = realms - dropDown.selectionAction = { [weak self] (index, item) in + dropDown.selectionAction = { [weak self] (_, item) in self?.realmsSelectionButton.setTitle(item, for: .normal) } - + realmsSelectionButton.isHidden = false realmTextInput.isHidden = true } else { @@ -73,39 +72,34 @@ class WizardRealmViewController: UIViewController { } } - + @IBAction func selectRealm(_ sender: AnyObject) { dropDown.show() } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == Segues.goToWebView { - let orViewController = segue.destination as! ORViewcontroller - switch configManager!.state { case .complete(let project): + guard let orViewController = segue.destination as? ORViewcontroller else { + fatalError("Invalid state for segue") + } orViewController.targetUrl = project.targetUrl default: fatalError("We should never come to this screen in that state") } - - + // TODO: based on configManager?.appInfos retrieve providers -> this is done by selectRealm on ConfigMgr - - - + // orViewController.targetUrl = "https://demo.openremote.io/manager/?realm=smartcity&consoleProviders=geofence push storage&consoleAutoEnable=true#!geofences" } } - + @IBAction func nextButtonpressed(_ sender: UIButton) { - // TODO: handle errors and have a proper error message - - + // TODO: handle the case realm is selected from menu - - + let state = try? configManager!.setRealm(realm: realmName) switch state { case let .complete(project): @@ -130,10 +124,8 @@ class WizardRealmViewController: UIViewController { break // Fall through to error message } - // If we reached here, an error occured - - + // TODO: proper error message let alertView = UIAlertController(title: "Error", message: "TODO", preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) @@ -146,8 +138,8 @@ extension WizardRealmViewController: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if textField == realmTextInput.textField { - if let s = realmTextInput.textField.text { - realmName = s.replacingCharacters(in: Range(range, in: s)!, with: string).trimmingCharacters(in: .whitespacesAndNewlines) + if let originalString = realmTextInput.textField.text { + realmName = originalString.replacingCharacters(in: Range(range, in: originalString)!, with: string).trimmingCharacters(in: .whitespacesAndNewlines) } } @@ -187,7 +179,7 @@ extension WizardRealmViewController: UITextFieldDelegate { return false } - if textField == realmTextInput.textField, let realm = realmName { + if textField == realmTextInput.textField, realmName != nil { realmTextInput.textField.resignFirstResponder() // requestAppConfig(domain) } diff --git a/GenericApp/GenericApp/Utils/String+Utils.swift b/GenericApp/GenericApp/Utils/String+Utils.swift index 690a1b6..b358183 100644 --- a/GenericApp/GenericApp/Utils/String+Utils.swift +++ b/GenericApp/GenericApp/Utils/String+Utils.swift @@ -21,7 +21,7 @@ import Foundation import UIKit extension String { - func isUrl () -> Bool { + func isUrl() -> Bool { if let url = NSURL(string: self) { return UIApplication.shared.canOpenURL(url as URL) } diff --git a/GenericApp/NotificationService/NotificationService.swift b/GenericApp/NotificationService/NotificationService.swift index 7829e9e..fb62570 100644 --- a/GenericApp/NotificationService/NotificationService.swift +++ b/GenericApp/NotificationService/NotificationService.swift @@ -21,10 +21,11 @@ import UserNotifications import ORLib class NotificationService: UNNotificationServiceExtension { - + public var contentHandler: ((UNNotificationContent) -> Void)? public var bestAttemptContent: UNMutableNotificationContent? + // swiftlint:disable:next cyclomatic_complexity open override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) @@ -34,7 +35,7 @@ class NotificationService: UNNotificationServiceExtension { let categoryName = "openremoteNotification" bestAttemptContent.categoryIdentifier = categoryName - //Buttons + // Buttons if let buttonsString = bestAttemptContent.userInfo[DefaultsKey.buttonsKey] as? String { if let buttonsData = buttonsString.data(using: .utf8) { if let buttons = try? JSONDecoder().decode([ORPushNotificationButton].self, from: buttonsData) { @@ -50,14 +51,14 @@ class NotificationService: UNNotificationServiceExtension { } let category = UNNotificationCategory(identifier: categoryName, actions: notificationActions, intentIdentifiers: [], options: []) - let categories : Set = [category] + let categories: Set = [category] UNUserNotificationCenter.current().setNotificationCategories(categories) } } } - //Actions + // Actions if let actionString = bestAttemptContent.userInfo[DefaultsKey.actionKey] as? String { - if let actionsData = actionString.data(using: .utf8){ + if let actionsData = actionString.data(using: .utf8) { if let action = try? JSONDecoder().decode(ORPushNotificationAction.self, from: actionsData) { bestAttemptContent.userInfo[ActionType.appUrl] = action.url @@ -75,19 +76,18 @@ class NotificationService: UNNotificationServiceExtension { } } - contentHandler(bestAttemptContent.copy() as! UNNotificationContent) + contentHandler(bestAttemptContent.copy() as! UNNotificationContent) // swiftlint:disable:this force_cast } } - open override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. NSLog("NotifExtension Time has expired") if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { - //Actions + // Actions if let actionString = bestAttemptContent.userInfo[DefaultsKey.actionKey] as? String { - if let actionsData = actionString.data(using: .utf8){ + if let actionsData = actionString.data(using: .utf8) { if let action = try? JSONDecoder().decode(ORPushNotificationAction.self, from: actionsData) { bestAttemptContent.userInfo[ActionType.appUrl] = action.url