├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── codeql.yml │ └── main.yml ├── .gitignore ├── .sonarcloud.properties ├── .swiftlint.yml ├── ATTRIBUTIONS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── IBM Notifier Structure.pdf ├── Images ├── Alerts │ ├── alert_profile_jps_4.png │ ├── alert_profile_settings.png │ └── alert_profile_xml.png ├── Banners │ ├── banner.png │ ├── banner_simple.png │ └── banner_simple_no_button.png ├── DeepLinks │ └── deeplink_config.png ├── Home │ └── cover.png ├── Onboarding │ ├── onboarding_1.png │ ├── onboarding_2.png │ └── onboarding_3.png ├── Popup │ ├── checklist_1.png │ ├── checklist_2.png │ ├── checklist_3.png │ ├── datepicker_1.png │ ├── datepicker_2.png │ ├── drop_down_1.png │ ├── drop_down_2.png │ ├── html.png │ ├── html_whitebox.png │ ├── popup.png │ ├── popup_image.png │ ├── popup_input.png │ ├── popup_progressbar.png │ ├── popup_progressbar_done.png │ ├── popup_secured_input.png │ ├── popup_timer.png │ ├── popup_video.png │ ├── popup_whitebox.png │ ├── popup_whitebox_markdown.png │ └── slideshow_1.png ├── Rebranding │ ├── rebranding-1.png │ ├── rebranding-2.png │ ├── rebranding-3.png │ ├── rebranding-4.png │ ├── rebranding-5.png │ └── rebranding-6.png ├── Sign&Notarize │ ├── 0.png │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png └── SystemAlert │ └── systemalert.png ├── LICENSE ├── MAINTAINERS.md ├── Notification Agent Alert Tests └── NAATriggersTests.swift ├── Notification Agent Alerts ├── AppDelegate.swift ├── Base.lproj │ └── Main.storyboard ├── Extensions │ └── NotificationDispatch-Extension.swift ├── Info.plist ├── Notification_Agent_Alerts.entitlements └── Notification_Agent_Alerts_DEV.entitlements ├── Notification Agent Banner Tests └── NABTriggersTests.swift ├── Notification Agent Banners ├── AppDelegate.swift ├── Base.lproj │ └── Main.storyboard ├── Extensions │ └── NotificationDispatch-Extension.swift ├── Info.plist ├── Notification_Agent_Banners.entitlements └── Notification_Agent_Banners_DEV.entitlements ├── Notification Agent Core Tests ├── NACInteractiveEFCLControllerTests.swift └── NACTriggersTests.swift ├── Notification Agent Core ├── AppDelegate.swift ├── Controllers │ ├── Context.swift │ ├── DeepLinkEngine.swift │ ├── HelpBuilder.swift │ ├── NALogger.swift │ └── TaskManager.swift ├── Extensions │ ├── EFCLController-Extension.swift │ └── NotificationDispatch-Extension.swift ├── Info.plist ├── Notification_Agent_Core.entitlements └── Resources │ ├── ATTRIBUTIONS.md │ ├── NOTICES.rtf │ ├── PRIVACY POLICY.rtf │ └── TERMS AND CONDITIONS.rtf ├── Notification Agent Onboarding Tests └── NAOTriggersTests.swift ├── Notification Agent Onboarding UI Tests └── NAOUITests.swift ├── Notification Agent Onboarding ├── AppDelegate.swift ├── Base.lproj │ └── Main.storyboard ├── Controllers │ └── OnboardingInteractiveEFCLController.swift ├── Extensions │ └── NotificationDispatch-Extension.swift ├── Info.plist ├── Notification_Agent_Onboarding.entitlements └── Views │ ├── OnboardingView.swift │ ├── OnboardingViewModel.swift │ ├── PageView.swift │ └── PageViewModel.swift ├── Notification Agent Popup Tests └── NAPTriggersTests.swift ├── Notification Agent Popup UI Tests └── NAPUITests.swift ├── Notification Agent Popups ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── Common │ │ ├── Contents.json │ │ └── default_icon.imageset │ │ │ ├── Contents.json │ │ │ ├── icon_128x128 1.png │ │ │ ├── icon_128x128 2.png │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x 1.png │ │ │ ├── icon_128x128@2x 2.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_128x128@3x 1.png │ │ │ ├── icon_128x128@3x 2.png │ │ │ └── icon_128x128@3x.png │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Controllers │ ├── PopupInteractiveEFCLController.swift │ └── SystemAlertController.swift ├── Extensions │ └── NotificationDispatch-Extension.swift ├── Info.plist ├── Notification_Agent_Popups.entitlements └── Views │ ├── BodyLabels.swift │ ├── PopUpView.swift │ └── PopUpViewModel.swift ├── Notification Agent.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ ├── xcbaselines │ └── F47F5552249B8C1C006A0754.xcbaseline │ │ ├── 637884EE-58CB-47E9-AF0B-6447CD2D27AE.plist │ │ └── Info.plist │ └── xcschemes │ ├── IBM Notifier Alert.xcscheme │ ├── IBM Notifier Banner.xcscheme │ ├── IBM Notifier Onboarding.xcscheme │ ├── IBM Notifier Popup.xcscheme │ └── IBM Notifier.xcscheme ├── NotificationAgentTP.xctestplan ├── README.md ├── SECURITY.md ├── Shared ├── Controllers │ ├── BackPanelController.swift │ ├── Context.swift │ ├── EFCLController.swift │ ├── Environment.swift │ ├── InteractiveEFCLContoller.swift │ ├── NALogger.swift │ ├── NotificationDispatch.swift │ ├── ReplyHandler.swift │ ├── TaskManager.swift │ └── UserNotificationController.swift ├── Extensions │ ├── Binding-Extension.swift │ ├── Collection-Extension.swift │ ├── Data-Extension.swift │ ├── Decodable-Extensions.swift │ ├── EFCLController-Extension.swift │ ├── Int-Extension.swift │ ├── NSColor-Extension.swift │ ├── NSScreen-Extension.swift │ ├── Notification-Extension.swift │ ├── String-Extension.swift │ ├── UNNotificationAttachment-Extension.swift │ └── View-Extension.swift ├── Model │ ├── Common │ │ ├── ACVDecoder.swift │ │ ├── AppComponent.swift │ │ ├── Claims.swift │ │ ├── InfoSection.swift │ │ ├── LoadableNib.swift │ │ ├── NAError.swift │ │ ├── NAMedia.swift │ │ ├── PickerItem.swift │ │ ├── SharedSettings.swift │ │ ├── TaskObject.swift │ │ ├── Token.swift │ │ └── UserReplyType.swift │ └── UIObjects │ │ ├── ConfigurableParameters.swift │ │ ├── DynamicNotificationButton.swift │ │ ├── NotificationAccessoryElement.swift │ │ ├── NotificationButton.swift │ │ ├── NotificationObject.swift │ │ ├── OnboardingData.swift │ │ ├── PopupReminder.swift │ │ ├── ProgressState.swift │ │ ├── SwiftUIButtonState.swift │ │ └── ViewSpec.swift ├── Protocols │ ├── ControlActionClosureProtocol.swift │ └── InteractiveObjectProtocol.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x.png │ │ └── Contents.json │ └── en.lproj │ │ └── Localizable.strings ├── Utils │ ├── ActionTrampoline.swift │ └── Utils.swift └── Views │ ├── AccessoryViews │ ├── AppKit │ │ ├── AccessoryView.swift │ │ ├── HTMLAccessoryView.swift │ │ ├── MarkdownTextView.swift │ │ └── VideoAccessoryView.swift │ └── SwiftUI │ │ ├── AccessoryViewSource.swift │ │ ├── AccessoryViewWrapper.swift │ │ ├── DatePickerView.swift │ │ ├── InputView.swift │ │ ├── MediaView.swift │ │ ├── PickerView.swift │ │ ├── ProgressBar │ │ ├── ProgressBarView.swift │ │ └── ProgressBarViewModel.swift │ │ ├── SlideShowView.swift │ │ └── ViewRepresentable │ │ ├── HTMLView.swift │ │ ├── ImageViewRepresentable.swift │ │ ├── MarkdownView.swift │ │ └── PlayerView.swift │ ├── Base.lproj │ └── Main.storyboard │ ├── Buttons │ ├── CircleButton.swift │ ├── NativeButton.swift │ └── StandardButton.swift │ ├── Common │ ├── BackPanelWindow.swift │ ├── FlippedStackView.swift │ ├── HorizontalLine.swift │ └── NoBackgroundScroller.swift │ └── Components │ ├── Icon.swift │ └── InfoSectionView.swift ├── example profiles ├── IBM Notifier DeepLink Security.mobileconfig └── IBM Notifier Notifications.mobileconfig ├── jwt-generator ├── Pipfile ├── README.md ├── jwtgenerator.py ├── keygen.sh └── requirements.txt └── renovate.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS 11] 28 | - Project version [e.g. 1.0.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | *Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* 2 | 3 | *List which issues are fixed by this PR. You must list at least one issue.* 4 | 5 | 6 | ## Pre-launch Checklist 7 | 8 | - [ ] I read the [Code of Conduct](/CODE_OF_CONDUCT.md) and followed the process outlined there for submitting PRs. 9 | - [ ] I listed at least one issue that this PR fixes in the description above. 10 | - [ ] I updated/added relevant documentation (doc comments with `///`). 11 | - [ ] I updated [README.md](https://github.com/IBM/mac-ibm-notifications/blob/main/README.md) file with new version/info - if applicable. 12 | - [ ] I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. 13 | - [ ] All existing and new tests are passing. 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | linting: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: GitHub Action for SwiftLint 15 | uses: norio-nomura/action-swiftlint@3.2.1 16 | test: 17 | runs-on: macos-14 18 | steps: 19 | - name: Xcode Setup 20 | uses: maxim-lobanov/setup-xcode@v1.6.0 21 | with: 22 | xcode-version: latest-stable 23 | - name: Checkout project 24 | uses: actions/checkout@v4 25 | - name: Run Xcodebuild Test, Build 26 | shell: bash --noprofile --norc -eo pipefail {0} 27 | run: | 28 | xcodebuild -project "Notification Agent.xcodeproj" -scheme "IBM Notifier" -destination 'platform=OS X' clean test build | xcpretty 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | 4 | ## User settings 5 | **/xcuserdata/ 6 | 7 | ## Obj-C/Swift specific 8 | *.hmap 9 | 10 | ## App packaging 11 | **/*.ipa 12 | **/*.dSYM.zip 13 | **/*.dSYM 14 | 15 | 16 | .build/ 17 | **/.DS_Store 18 | **/*.zip 19 | **/*.app 20 | 21 | fastlane/test_output/ 22 | fastlane/report.xml 23 | fastlane/README.md 24 | Gemfile.lock 25 | .idea/ 26 | Notification Agent.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved 27 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | # Python version (for python projects only) 2 | sonar.python.version=3 3 | # Project Version to be used as Code Definition 4 | sonar.projectVersion=3.2.1.127 5 | # Folders excluded from the scan 6 | sonar.exclusions=**/Notification Agent Core Tests/**,**/Notification Agent Alert Tests/**,**/Notification Agent Banner Tests/**,**/Notification Agent Popup Tests/**,**/Notification Agent Onboarding Tests/**,**/Notification Agent Popup UI Tests/**,**/Notification Agent Onboarding UI Tests/**,**/Notification Agent Core/Controllers/HelpBuilder.swift** 7 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | disabled_rules: 3 | - line_length 4 | - compiler_protocol_init 5 | - cyclomatic_complexity 6 | - notification_center_detachment 7 | - trailing_whitespace 8 | - colon -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 2.9.1 Build 96 2 | 3 | ## Pop-up 4 | ### Enhancements 5 | * The default Pop-up UI icon resolution have been optimised - Close #116 6 | ### Resolved in this build 7 | * Fixed: `secureinput` accessory view appear higher than normal when used in some scenarios - Close #112 8 | * Fixed: main button still shows "Cancel" label when progress bar end - Close #128 9 | * Fixed: issue with the representation of UTF8 special characters in `html`/`htmlwhitebox` Accessory Views 10 | 11 | ## Alert/Banner 12 | ### Resolved in this build 13 | * Fixed: Notification Center Alerts/Banners not pull attention away from keyboard when appears - Close #98 14 | 15 | ## Onboarding 16 | ### Resolved in this build 17 | * Fixed: duplication of the onboarding page content when window is minimised - Close #121 18 | * Fixed: missing update of common Onboarding UI progress bar - Close #114 19 | 20 | ## SystemAlert 21 | ### Enhanchements 22 | * New SystemAlert UI available - [Doc](https://github.com/IBM/mac-ibm-notifications/wiki/Usage) 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing In General 2 | Our project welcomes external contributions. If you have an itch, please feel 3 | free to scratch it. 4 | 5 | To contribute code or documentation, please submit a [pull request](https://github.com/ibm/mac-ibm-notifications/pulls). 6 | 7 | A good way to familiarize yourself with the codebase and contribution process is 8 | to look for and tackle low-hanging fruit in the [issue tracker](https://github.com/ibm/mac-ibm-notifications/issues). 9 | Before embarking on a more ambitious contribution, please quickly [get in touch](#communication) with us. 10 | 11 | **Note: We appreciate your effort, and want to avoid a situation where a contribution 12 | requires extensive rework (by you or by us), sits in backlog for a long time, or 13 | cannot be accepted at all!** 14 | 15 | ### Proposing new features 16 | 17 | If you would like to implement a new feature, please [raise an issue](https://github.com/ibm/mac-ibm-notifications/issues) 18 | before sending a pull request so the feature can be discussed. This is to avoid 19 | you wasting your valuable time working on a feature that the project developers 20 | are not interested in accepting into the code base. 21 | 22 | ### Fixing bugs 23 | 24 | If you would like to fix a bug, please [raise an issue](https://github.com/ibm/mac-ibm-notifications/issues) before sending a 25 | pull request so it can be tracked. 26 | 27 | ### Merge approval 28 | 29 | The project maintainers use LGTM (Looks Good To Me) in comments on the code 30 | review to indicate acceptance. A change requires LGTMs from two of the 31 | maintainers of each component affected. 32 | 33 | For a list of the maintainers, see the [MAINTAINERS.md](MAINTAINERS.md) page. 34 | 35 | ## Legal 36 | 37 | Each source file must include a license header for the Apache 38 | Software License 2.0. Using the SPDX format is the simplest approach. 39 | e.g. 40 | 41 | ``` 42 | /* 43 | Copyright All Rights Reserved. 44 | 45 | SPDX-License-Identifier: Apache-2.0 46 | */ 47 | ``` 48 | 49 | We have tried to make it as easy as possible to make contributions. This 50 | applies to how we handle the legal aspects of contribution. We use the 51 | same approach - the [Developer's Certificate of Origin 1.1 (DCO)](https://github.com/hyperledger/fabric/blob/master/docs/source/DCO1.1.txt) - that the Linux® Kernel [community](https://elinux.org/Developer_Certificate_Of_Origin) 52 | uses to manage code contributions. 53 | 54 | We simply ask that when submitting a patch for review, the developer 55 | must include a sign-off statement in the commit message. 56 | 57 | Here is an example Signed-off-by line, which indicates that the 58 | submitter accepts the DCO: 59 | 60 | ``` 61 | Signed-off-by: John Doe 62 | ``` 63 | 64 | You can include this automatically when you commit a change to your 65 | local git repository using the following command: 66 | 67 | ``` 68 | git commit -s 69 | ``` 70 | 71 | ## Communication 72 | Please feel free to connect with us by mail, see the [MAINTAINERS.md](MAINTAINERS.md) page. 73 | 74 | ## Testing 75 | Make sure that every Xcode Unit and UI test pass befor submitting any changes. 76 | 77 | ## Coding style guidelines 78 | Please follow the SwiftLintFramework rules. 79 | -------------------------------------------------------------------------------- /IBM Notifier Structure.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/IBM Notifier Structure.pdf -------------------------------------------------------------------------------- /Images/Alerts/alert_profile_jps_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Alerts/alert_profile_jps_4.png -------------------------------------------------------------------------------- /Images/Alerts/alert_profile_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Alerts/alert_profile_settings.png -------------------------------------------------------------------------------- /Images/Alerts/alert_profile_xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Alerts/alert_profile_xml.png -------------------------------------------------------------------------------- /Images/Banners/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Banners/banner.png -------------------------------------------------------------------------------- /Images/Banners/banner_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Banners/banner_simple.png -------------------------------------------------------------------------------- /Images/Banners/banner_simple_no_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Banners/banner_simple_no_button.png -------------------------------------------------------------------------------- /Images/DeepLinks/deeplink_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/DeepLinks/deeplink_config.png -------------------------------------------------------------------------------- /Images/Home/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Home/cover.png -------------------------------------------------------------------------------- /Images/Onboarding/onboarding_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Onboarding/onboarding_1.png -------------------------------------------------------------------------------- /Images/Onboarding/onboarding_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Onboarding/onboarding_2.png -------------------------------------------------------------------------------- /Images/Onboarding/onboarding_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Onboarding/onboarding_3.png -------------------------------------------------------------------------------- /Images/Popup/checklist_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/checklist_1.png -------------------------------------------------------------------------------- /Images/Popup/checklist_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/checklist_2.png -------------------------------------------------------------------------------- /Images/Popup/checklist_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/checklist_3.png -------------------------------------------------------------------------------- /Images/Popup/datepicker_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/datepicker_1.png -------------------------------------------------------------------------------- /Images/Popup/datepicker_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/datepicker_2.png -------------------------------------------------------------------------------- /Images/Popup/drop_down_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/drop_down_1.png -------------------------------------------------------------------------------- /Images/Popup/drop_down_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/drop_down_2.png -------------------------------------------------------------------------------- /Images/Popup/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/html.png -------------------------------------------------------------------------------- /Images/Popup/html_whitebox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/html_whitebox.png -------------------------------------------------------------------------------- /Images/Popup/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup.png -------------------------------------------------------------------------------- /Images/Popup/popup_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_image.png -------------------------------------------------------------------------------- /Images/Popup/popup_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_input.png -------------------------------------------------------------------------------- /Images/Popup/popup_progressbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_progressbar.png -------------------------------------------------------------------------------- /Images/Popup/popup_progressbar_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_progressbar_done.png -------------------------------------------------------------------------------- /Images/Popup/popup_secured_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_secured_input.png -------------------------------------------------------------------------------- /Images/Popup/popup_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_timer.png -------------------------------------------------------------------------------- /Images/Popup/popup_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_video.png -------------------------------------------------------------------------------- /Images/Popup/popup_whitebox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_whitebox.png -------------------------------------------------------------------------------- /Images/Popup/popup_whitebox_markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/popup_whitebox_markdown.png -------------------------------------------------------------------------------- /Images/Popup/slideshow_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Popup/slideshow_1.png -------------------------------------------------------------------------------- /Images/Rebranding/rebranding-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Rebranding/rebranding-1.png -------------------------------------------------------------------------------- /Images/Rebranding/rebranding-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Rebranding/rebranding-2.png -------------------------------------------------------------------------------- /Images/Rebranding/rebranding-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Rebranding/rebranding-3.png -------------------------------------------------------------------------------- /Images/Rebranding/rebranding-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Rebranding/rebranding-4.png -------------------------------------------------------------------------------- /Images/Rebranding/rebranding-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Rebranding/rebranding-5.png -------------------------------------------------------------------------------- /Images/Rebranding/rebranding-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Rebranding/rebranding-6.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/0.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/1.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/10.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/2.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/3.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/4.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/5.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/6.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/7.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/8.png -------------------------------------------------------------------------------- /Images/Sign&Notarize/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/Sign&Notarize/9.png -------------------------------------------------------------------------------- /Images/SystemAlert/systemalert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Images/SystemAlert/systemalert.png -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # MAINTAINERS 2 | 3 | Simone Martorelli - simone.martorelli@ibm.com 4 | 5 | Jan Valentik - jvalentik@sk.ibm.com 6 | -------------------------------------------------------------------------------- /Notification Agent Alert Tests/NAATriggersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NAATriggersTests.swift 3 | // Notification Agent Alert Tests 4 | // 5 | // Created by Simone Martorelli on 01/06/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import XCTest 11 | 12 | class NAATriggersTests: XCTestCase { 13 | 14 | let worker = TestWorker() 15 | let parserUseCases = [["type" : "alert", "title" : "This is a title"], 16 | ["type" : "alert", "subtitle" : "This is a subtitle"], 17 | ["type" : "alert", "title" : "This is a title", "subtitle" : "This is a subtitle"], 18 | ["type" : "alert", "workflow" : "resetAlerts"]] 19 | 20 | override func setUpWithError() throws { 21 | worker.startObservation() 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | worker.stopObservation() 26 | } 27 | 28 | func testArgumentParsing() throws { 29 | for useCase in parserUseCases { 30 | guard let notificationObject = try? NotificationObject(from: useCase), 31 | let settings = Context.main.sharedSettings else { 32 | XCTAssert(false, "Failed use case: \(useCase)") 33 | return 34 | } 35 | let taskObject = TaskObject(notification: notificationObject, settings: settings) 36 | guard let jsonData = try? JSONEncoder().encode(taskObject) else { 37 | XCTAssert(false, "Failed use case: \(useCase)") 38 | return 39 | } 40 | EFCLController.shared.parseArguments([jsonData.base64EncodedString()]) 41 | XCTAssert(worker.argumentsSuccessfullyParsed, "Failed use case: \(useCase)") 42 | worker.argumentsSuccessfullyParsed = false 43 | } 44 | } 45 | } 46 | 47 | extension NAATriggersTests { 48 | class TestWorker { 49 | var argumentsSuccessfullyParsed: Bool 50 | 51 | init() { 52 | argumentsSuccessfullyParsed = false 53 | } 54 | 55 | /// Adding notification observer 56 | func startObservation() { 57 | NotificationCenter.default.addObserver(self, 58 | selector: #selector(receivedNotification), 59 | name: .showNotification, 60 | object: nil) 61 | } 62 | 63 | /// Removing notification observer 64 | func stopObservation() { 65 | NotificationCenter.default.removeObserver(self, name: .showNotification, object: nil) 66 | } 67 | 68 | @objc func receivedNotification(_ notification: NSNotification) { 69 | argumentsSuccessfullyParsed = true 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Notification Agent Alerts/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 18/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | import os.log 12 | 13 | @NSApplicationMain 14 | class AppDelegate: NSObject, NSApplicationDelegate { 15 | let userNotificationController = UserNotificationController.shared 16 | let notificationDispatch = NotificationDispatch.shared 17 | let efclController = EFCLController.shared 18 | let context = Context.main 19 | let logger = NALogger.shared 20 | var isConfigured: Bool = false 21 | 22 | private func configureApp(_ completion: @escaping () -> Void = {}) { 23 | guard !isConfigured else { 24 | completion() 25 | return 26 | } 27 | isConfigured = true 28 | notificationDispatch.startObservingForNotifications() 29 | guard !UserNotificationController.shared.agentTriggeredByNotificationCenter else { 30 | completion() 31 | return 32 | } 33 | efclController.parseArguments() 34 | completion() 35 | } 36 | 37 | func applicationDidFinishLaunching(_ aNotification: Notification) { 38 | configureApp() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Notification Agent Alerts/Extensions/NotificationDispatch-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationDispatch-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 5/27/21. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Cocoa 12 | 13 | extension NotificationDispatch { 14 | /// Handle the received notification and send the notification object to the correct controller. 15 | /// - Parameter notification: the received notification. 16 | @objc 17 | func receivedNotification(_ notification: Notification) { 18 | guard let object = notification.userInfo?["object"] as? NotificationObject else { return } 19 | UserNotificationController.shared.showBanner(object) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Notification Agent Alerts/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(TARGET_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | ITSAppUsesNonExemptEncryption 22 | 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSHumanReadableCopyright 35 | © Copyright IBM Corp. 2021, 2024 36 | NSMainStoryboardFile 37 | Main 38 | NSPrincipalClass 39 | NSApplication 40 | NSSupportsAutomaticTermination 41 | 42 | NSSupportsSuddenTermination 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Notification Agent Alerts/Notification_Agent_Alerts.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.disable-library-validation 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent Alerts/Notification_Agent_Alerts_DEV.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.disable-library-validation 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent Banner Tests/NABTriggersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NABTriggersTests.swift 3 | // Notification Agent Alert Tests 4 | // 5 | // Created by Simone Martorelli on 01/06/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import XCTest 11 | 12 | class NABTriggersTests: XCTestCase { 13 | 14 | let worker = TestWorker() 15 | let parserUseCases = [["type" : "banner", "title" : "This is a title"], 16 | ["type" : "banner", "subtitle" : "This is a subtitle"], 17 | ["type" : "banner", "title" : "This is a title", "subtitle" : "This is a subtitle"], 18 | ["type" : "banner", "workflow" : "resetBanners"]] 19 | 20 | override func setUpWithError() throws { 21 | worker.startObservation() 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | worker.stopObservation() 26 | } 27 | 28 | func testArgumentParsing() throws { 29 | for useCase in parserUseCases { 30 | guard let notificationObject = try? NotificationObject(from: useCase), 31 | let settings = Context.main.sharedSettings else { 32 | XCTAssert(false, "Failed use case: \(useCase)") 33 | return 34 | } 35 | let taskObject = TaskObject(notification: notificationObject, settings: settings) 36 | guard let jsonData = try? JSONEncoder().encode(taskObject) else { 37 | XCTAssert(false, "Failed use case: \(useCase)") 38 | return 39 | } 40 | EFCLController.shared.parseArguments([jsonData.base64EncodedString()]) 41 | XCTAssert(worker.argumentsSuccessfullyParsed, "Failed use case: \(useCase)") 42 | worker.argumentsSuccessfullyParsed = false 43 | } 44 | } 45 | } 46 | 47 | extension NABTriggersTests { 48 | class TestWorker { 49 | var argumentsSuccessfullyParsed: Bool 50 | 51 | init() { 52 | argumentsSuccessfullyParsed = false 53 | } 54 | 55 | /// Adding notification observer 56 | func startObservation() { 57 | NotificationCenter.default.addObserver(self, 58 | selector: #selector(receivedNotification), 59 | name: .showNotification, 60 | object: nil) 61 | } 62 | 63 | /// Removing notification observer 64 | func stopObservation() { 65 | NotificationCenter.default.removeObserver(self, name: .showNotification, object: nil) 66 | } 67 | 68 | @objc func receivedNotification(_ notification: NSNotification) { 69 | argumentsSuccessfullyParsed = true 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Notification Agent Banners/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notification Agent 4 | // 5 | // Created by Jan Valentik on 18/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | import os.log 12 | 13 | @NSApplicationMain 14 | class AppDelegate: NSObject, NSApplicationDelegate { 15 | let userNotificationController = UserNotificationController.shared 16 | let notificationDispatch = NotificationDispatch.shared 17 | let efclController = EFCLController.shared 18 | let context = Context.main 19 | var isConfigured: Bool = false 20 | 21 | private func configureApp(_ completion: @escaping () -> Void = {}) { 22 | guard !isConfigured else { 23 | completion() 24 | return 25 | } 26 | isConfigured = true 27 | notificationDispatch.startObservingForNotifications() 28 | guard !UserNotificationController.shared.agentTriggeredByNotificationCenter else { 29 | completion() 30 | return 31 | } 32 | efclController.parseArguments() 33 | completion() 34 | } 35 | 36 | func applicationDidFinishLaunching(_ aNotification: Notification) { 37 | configureApp() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Notification Agent Banners/Extensions/NotificationDispatch-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationDispatch-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 5/27/21. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Cocoa 12 | 13 | extension NotificationDispatch { 14 | /// Handle the received notification and send the notification object to the correct controller. 15 | /// - Parameter notification: the received notification. 16 | @objc 17 | func receivedNotification(_ notification: Notification) { 18 | guard let object = notification.userInfo?["object"] as? NotificationObject else { return } 19 | UserNotificationController.shared.showBanner(object) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Notification Agent Banners/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(TARGET_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | ITSAppUsesNonExemptEncryption 22 | 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSHumanReadableCopyright 35 | © Copyright IBM Corp. 2021, 2024 36 | NSMainStoryboardFile 37 | Main 38 | NSPrincipalClass 39 | NSApplication 40 | NSSupportsAutomaticTermination 41 | 42 | NSSupportsSuddenTermination 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Notification Agent Banners/Notification_Agent_Banners.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.disable-library-validation 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent Banners/Notification_Agent_Banners_DEV.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.disable-library-validation 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent Core Tests/NACInteractiveEFCLControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NACInteractiveEFCLControllerTests.swift 3 | // Notification Agent Core Tests 4 | // 5 | // Created by Simone Martorelli on 27/05/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import XCTest 11 | 12 | class NACInteractiveEFCLControllerTests: XCTestCase { 13 | 14 | func testInteractiveEFCLPipe() throws { 15 | let controller = InteractiveEFCLController() 16 | controller.startObservingStandardInput() 17 | XCTAssert(controller.inputPipe != nil) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Notification Agent Core Tests/NACTriggersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NACTriggersTests.swift 3 | // Notification Agent Core Tests 4 | // 5 | // Created by Simone Martorelli on 26/05/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import XCTest 11 | 12 | class NACTriggersTests: XCTestCase { 13 | 14 | let worker = TestWorker() 15 | let parserUseCases = [["toBeRemoved", "-type", "popup", "-title", "This is a title"], 16 | ["toBeRemoved", "-type", "banner", "-title", "This is a title"], 17 | ["toBeRemoved", "-type", "alert", "-title", "This is a title"], 18 | ["toBeRemoved", "-type", "popup", "-title", "This is a title", "-silent", "-subtitle", "This is a subtitle"]] 19 | let processerUseCases = [URL(string: "ibmnotifier:shownotification?type=popup&title=This%20is%20a%20title")!, 20 | URL(string: "ibmnotifier:shownotification?type=banner&title=This%20is%20a%20title")!, 21 | URL(string: "ibmnotifier:shownotification?type=alert&title=This%20is%20a%20title")!, 22 | URL(string: "ibmnotifier:shownotification?type=alert&title=title&silent&subtitle=This%20is%20a%20subtitle")!] 23 | 24 | override func setUpWithError() throws { 25 | worker.startObservation() 26 | } 27 | 28 | override func tearDownWithError() throws { 29 | worker.stopObservation() 30 | } 31 | 32 | func testArgumentParsing() throws { 33 | for useCase in parserUseCases { 34 | EFCLController.shared.parseArguments(useCase) 35 | XCTAssert(worker.argumentsSuccessfullyParsed, "Failed use case: \(useCase)") 36 | worker.argumentsSuccessfullyParsed = false 37 | } 38 | } 39 | 40 | func testURLProcessing() throws { 41 | for useCase in processerUseCases { 42 | DeepLinkEngine.shared.processURL(useCase) 43 | XCTAssert(worker.argumentsSuccessfullyParsed, "Failed use case: \(useCase)") 44 | worker.argumentsSuccessfullyParsed = false 45 | } 46 | } 47 | } 48 | 49 | extension NACTriggersTests { 50 | class TestWorker { 51 | var argumentsSuccessfullyParsed: Bool 52 | 53 | init() { 54 | argumentsSuccessfullyParsed = false 55 | } 56 | 57 | /// Adding notification observer 58 | func startObservation() { 59 | NotificationCenter.default.addObserver(self, 60 | selector: #selector(receivedNotification), 61 | name: .showNotification, 62 | object: nil) 63 | } 64 | 65 | /// Removing notification observer 66 | func stopObservation() { 67 | NotificationCenter.default.removeObserver(self, name: .showNotification, object: nil) 68 | } 69 | 70 | @objc func receivedNotification(_ notification: NSNotification) { 71 | argumentsSuccessfullyParsed = true 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Notification Agent Core/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notification Agent 4 | // 5 | // Created by Jan Valentik on 18/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | import os.log 12 | import Signals 13 | import ServiceManagement 14 | 15 | @NSApplicationMain 16 | class AppDelegate: NSObject, NSApplicationDelegate { 17 | let notificationDispatch = NotificationDispatch.shared 18 | let deepLinkEngine = DeepLinkEngine.shared 19 | let efclController = EFCLController.shared 20 | let context = Context.main 21 | var isConfigured: Bool = false 22 | 23 | private func configureApp(_ completion: @escaping () -> Void = {}) { 24 | guard !isConfigured else { 25 | completion() 26 | return 27 | } 28 | isConfigured = true 29 | notificationDispatch.startObservingForNotifications() 30 | efclController.parseArguments() 31 | completion() 32 | } 33 | 34 | func applicationDidFinishLaunching(_ aNotification: Notification) { 35 | Signals.trap(signal: .user(Int(SIGINT))) { _ in 36 | let ops = OperationQueue() 37 | ops.addOperation { 38 | NotificationDispatch.shared.taskManager.interruptUITasks() 39 | } 40 | ops.waitUntilAllOperationsAreFinished() 41 | EFCLController.shared.applicationExit(withReason: .receivedSigInt) 42 | } 43 | configureApp() 44 | } 45 | 46 | func application(_ application: NSApplication, open urls: [URL]) { 47 | self.deepLinkEngine.agentTriggeredByDeepLink = true 48 | guard UserDefaults.standard.bool(forKey: "deeplinkSecurity") else { 49 | NALogger.shared.log("You need to enable deep link security to use deep link") 50 | EFCLController.shared.applicationExit(withReason: .unableToLoadResources) 51 | return 52 | } 53 | configureApp { 54 | for url in urls { 55 | NALogger.shared.log("IBM Notifier has been triggered by a URL", [url.absoluteString]) 56 | self.deepLinkEngine.processURL(url) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Notification Agent Core/Controllers/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Context.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 31/03/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Application context 13 | final class Context { 14 | 15 | static let main: Context = Context() 16 | 17 | // MARK: - Variables 18 | 19 | var sharedSettings: SharedSettings 20 | 21 | // MARK: - Initializers 22 | 23 | init() { 24 | self.sharedSettings = SharedSettings(isVerboseModeEnabled: false, 25 | environment: Environment.current) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Notification Agent Core/Controllers/DeepLinkEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLinkEngine.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 6/22/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import os.log 12 | import SwiftJWT 13 | 14 | /// Handle the deeplink that triggered the agent. 15 | final class DeepLinkEngine { 16 | 17 | // MARK: - Static constants 18 | 19 | static let shared = DeepLinkEngine() 20 | 21 | // MARK: - Constants 22 | 23 | private let notificationPath: String = "shownotification" 24 | let context = Context.main 25 | var agentTriggeredByDeepLink: Bool = false 26 | 27 | // MARK: - Public Methods 28 | 29 | /// Process the received url and propagate a notification if can build a correct notification object from it. 30 | /// - Parameter url: the received url. 31 | func processURL(_ url: URL) { 32 | NALogger.shared.log("Parsing the received URL") 33 | do { 34 | let notificationObject = try parse(url) 35 | NALogger.shared.log("Ended parsing received URL") 36 | 37 | // Propagates the received notification 38 | NotificationCenter.default.post(name: .showNotification, object: self, userInfo: ["object" : notificationObject]) 39 | } catch { 40 | NALogger.shared.log("Error: %{public}@. No UI will be showed for the URL", [error.localizedDescription]) 41 | } 42 | } 43 | 44 | // MARK: - Private Methods 45 | 46 | /// Parse the received url and returns a notification object. 47 | /// - Parameter url: url received. 48 | /// - Throws: parsing errors. 49 | /// - Returns: notification object to be showed to the user. 50 | private func parse(_ url: URL) throws -> NotificationObject { 51 | // Parse the URL - BEGIN 52 | guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true) else { 53 | throw NAError.deepLinkHandling(type: .failedToGetComponents) 54 | } 55 | 56 | // Get the URL path. 57 | guard let path = components.path, path == notificationPath else { 58 | throw NAError.deepLinkHandling(type: .unsupportedPath) 59 | } 60 | 61 | // Get the URL parameters. 62 | guard let params = components.queryItems else { 63 | throw NAError.deepLinkHandling(type: .noParametersFound) 64 | } 65 | 66 | // Transform [QueryItems] in [String: String] 67 | var dict: [String: String] = [:] 68 | dict = params.reduce(into: [:]) {(dict, item) in 69 | return dict[item.name] = item.value ?? "" 70 | } 71 | dict = dict.filter({ $0.value != "" }) 72 | if UserDefaults.standard.bool(forKey: "deeplinkSecurity") { 73 | guard let token = dict["token"], verifyToken(token: Token(value: token)) else { 74 | throw NAError.deepLinkHandling(type: .invalidToken) 75 | } 76 | } 77 | // Parse the URL - END 78 | 79 | let notificationObject = try NotificationObject(from: dict) 80 | return notificationObject 81 | } 82 | 83 | private func verifyToken(token: Token) -> Bool { 84 | guard let publicKeyString = UserDefaults.standard.string(forKey: "deeplinkSecurityKey"), 85 | let publickKeyData = publicKeyString.data(using: .utf8) else { return false } 86 | let jwtVerifier = JWTVerifier.rs256(publicKey: publickKeyData) 87 | guard let verified = try? JWT(jwtString: token.value, verifier: jwtVerifier) else { 88 | return false 89 | } 90 | let validationResult = verified.validateClaims() 91 | 92 | return validationResult == .success && JWT.verify(token.value, using: jwtVerifier) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Notification Agent Core/Controllers/NALogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 8/27/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import os.log 12 | 13 | /// A simple class based on Apple os.log that handle normal and verbose logs. 14 | public final class NALogger { 15 | 16 | // MARK: - Static Variables 17 | 18 | static let shared = NALogger() 19 | 20 | // MARK: - Methods 21 | 22 | func log(_ type: OSLogType, _ message: StaticString, _ args: [String] = []) { 23 | Logger().log(level: type, 24 | "\(String(format: message.description.replacingOccurrences(of: "{public}", with: ""), arguments: args), privacy: .public)") 25 | if Context.main.sharedSettings.isVerboseModeEnabled || type == .error { 26 | self.verbose(type, message, args) 27 | } 28 | } 29 | func log(_ message: StaticString, _ args: [String] = []) { 30 | Logger().log(level: .default, 31 | "\(String(format: message.description.replacingOccurrences(of: "{public}", with: ""), arguments: args), privacy: .public)") 32 | if Context.main.sharedSettings.isVerboseModeEnabled { 33 | self.verbose(.default, message, args) 34 | } 35 | } 36 | func deprecationLog(since version: AppVersion, deprecatedArgument: String) { 37 | if version.isFinalDeprecatedVersion() { 38 | self.log(.error, "The following argument has been deprecated: %{public}@. Please update your workflow.", [deprecatedArgument]) 39 | } else { 40 | self.log("The following argument has been deprecated and will not be supported anymore soon: %{public}@. Make sure to update your workflow as soon as possible.", [deprecatedArgument]) 41 | } 42 | } 43 | 44 | // MARK: - Private Methods 45 | 46 | private func verbose(_ type: OSLogType, _ message: StaticString, _ args: [String] = []) { 47 | let message = type == .error ? 48 | message.description.replacingOccurrences(of: "{public}", with: "").red() : 49 | message.description.replacingOccurrences(of: "{public}", with: "").yellow() 50 | 51 | print(String(format: message, arguments: args)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Notification Agent Core/Extensions/NotificationDispatch-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationDispatch-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 28/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension NotificationDispatch { 13 | /// Handle the received notification and send the notification object to the correct controller. 14 | /// - Parameter notification: the received notification. 15 | @objc 16 | func receivedNotification(_ notification: Notification) { 17 | guard let notificationObject = notification.userInfo?["object"] as? NotificationObject else { 18 | EFCLController.shared.applicationExit(withReason: .internalError) 19 | return 20 | } 21 | let object = TaskObject(notification: notificationObject, settings: Context.main.sharedSettings) 22 | guard let jsonData = try? JSONEncoder().encode(object) else { 23 | EFCLController.shared.applicationExit(withReason: .internalError) 24 | return 25 | } 26 | switch object.notification.type { 27 | case .banner: 28 | taskManager.runAsyncTaskOnComponent(.banner, with: jsonData) { terminationStatus in 29 | exit(terminationStatus) 30 | } 31 | case .popup, .systemalert: 32 | var isInteractive: Bool { 33 | var int = false 34 | object.notification.accessoryViews?.forEach({ accessoryView in 35 | if accessoryView.type == .progressbar { 36 | int = true 37 | } 38 | }) 39 | int = int || object.notification.warningButton != nil 40 | return int 41 | } 42 | taskManager.runAsyncTaskOnComponent(.popup, with: jsonData, isInteractive: isInteractive) { terminationStatus in 43 | exit(terminationStatus) 44 | } 45 | case .onboarding: 46 | var isInteractive: Bool { 47 | return object.notification.payload?.progressBarPayload != nil 48 | } 49 | taskManager.runAsyncTaskOnComponent(.onboarding, with: jsonData, isInteractive: isInteractive) { terminationStatus in 50 | exit(terminationStatus) 51 | } 52 | case .alert: 53 | taskManager.runAsyncTaskOnComponent(.alert, with: jsonData) { terminationStatus in 54 | exit(terminationStatus) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Notification Agent Core/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | Viewer 24 | CFBundleURLName 25 | NotificationAgentDeepLink 26 | CFBundleURLSchemes 27 | 28 | ibmnotifier 29 | 30 | 31 | 32 | CFBundleVersion 33 | $(CURRENT_PROJECT_VERSION) 34 | ITSAppUsesNonExemptEncryption 35 | 36 | LSApplicationCategoryType 37 | public.app-category.utilities 38 | LSMinimumSystemVersion 39 | $(MACOSX_DEPLOYMENT_TARGET) 40 | LSUIElement 41 | 42 | NSAppTransportSecurity 43 | 44 | NSAllowsArbitraryLoads 45 | 46 | 47 | NSHumanReadableCopyright 48 | © Copyright IBM Corp. 2021, 2024 49 | NSMainStoryboardFile 50 | Main 51 | NSPrincipalClass 52 | NSApplication 53 | NSSupportsAutomaticTermination 54 | 55 | NSSupportsSuddenTermination 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Notification Agent Core/Notification_Agent_Core.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.disable-library-validation 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent Core/Resources/PRIVACY POLICY.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2580 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 IBMPlexSans;} 3 | {\colortbl;\red255\green255\blue255;\red38\green38\blue38;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;\cssrgb\c19608\c19608\c19608;\cssrgb\c100000\c100000\c100000;} 5 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \deftab720 7 | \pard\pardeftab720\partightenfactor0 8 | 9 | \f0\fs60 \cf2 \cb3 \expnd0\expndtw0\kerning0 10 | Privacy Statement\ 11 | } -------------------------------------------------------------------------------- /Notification Agent Core/Resources/TERMS AND CONDITIONS.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2580 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 IBMPlexSans;\f1\froman\fcharset0 Times-Roman;} 3 | {\colortbl;\red255\green255\blue255;\red38\green38\blue38;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;\cssrgb\c19608\c19608\c19608;\cssrgb\c100000\c100000\c100000;} 5 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \deftab720 7 | \pard\pardeftab720\partightenfactor0 8 | 9 | \f0\fs60 \cf2 \cb3 \expnd0\expndtw0\kerning0 10 | TERMS AND CONDITIONS 11 | \f1\fs24 \cf2 \ 12 | } -------------------------------------------------------------------------------- /Notification Agent Onboarding/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 18/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | import os.log 12 | 13 | @NSApplicationMain 14 | class AppDelegate: NSObject, NSApplicationDelegate { 15 | let notificationDispatch = NotificationDispatch.shared 16 | let efclController = EFCLController.shared 17 | let context = Context.main 18 | var isConfigured: Bool = false 19 | 20 | private func configureApp(_ completion: @escaping () -> Void = {}) { 21 | guard !isConfigured else { 22 | completion() 23 | return 24 | } 25 | isConfigured = true 26 | NSApplication.shared.activate(ignoringOtherApps: true) 27 | notificationDispatch.startObservingForNotifications() 28 | efclController.parseArguments() 29 | AppComponent.current.cleanSavedFiles() 30 | completion() 31 | } 32 | 33 | func applicationDidFinishLaunching(_ aNotification: Notification) { 34 | // Intercept the command+q shortcut and modify the exit value to reflect a manual user dismission. 35 | NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in 36 | switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) { 37 | case [.command] where event.characters == "q": 38 | guard !self.context.disableQuit else { return .none } 39 | Utils.applicationExit(withReason: .userDismissedOnboarding) 40 | default: 41 | return event 42 | } 43 | return event 44 | } 45 | configureApp() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Notification Agent Onboarding/Controllers/OnboardingInteractiveEFCLController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingInteractiveEFCLController.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 24/06/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | final class OnboardingInteractiveEFCLController: InteractiveEFCLController { 13 | override func processInput(_ notification: Notification) { 14 | let inputData = inputPipe.availableData 15 | if !inputData.isEmpty { 16 | guard let strData = String(data: inputData, encoding: String.Encoding.utf8)?.trimmingCharacters(in: CharacterSet.newlines) else { return } 17 | let splittedStrings = strData.split(separator: "/") 18 | for string in splittedStrings { 19 | guard let argument = string.split(separator: " ", maxSplits: 1).first?.lowercased(), 20 | var value = string.split(separator: " ", maxSplits: 1).last else { continue } 21 | if value.last == " " { 22 | value.removeLast() 23 | } 24 | switch argument { 25 | case "percent", "top_message", "bottom_message", "user_interaction_enabled", "user_interruption_allowed", "exit_on_completion", "end": 26 | NotificationCenter.default.post(name: Notification.Name("progressbar_interactive_updates"), object: nil, userInfo: ["data" : inputData]) 27 | default: 28 | continue 29 | } 30 | } 31 | } 32 | inputPipe.waitForDataInBackgroundAndNotify() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Notification Agent Onboarding/Extensions/NotificationDispatch-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationDispatch-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 5/27/21. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Cocoa 12 | import SwiftUI 13 | 14 | extension NotificationDispatch { 15 | /// Handle the received notification and send the notification object to the correct controller. 16 | /// - Parameter notification: the received notification. 17 | @objc 18 | func receivedNotification(_ notification: Notification) { 19 | guard let object = notification.userInfo?["object"] as? NotificationObject else { return } 20 | guard let onboardingData = object.payload else { return } 21 | DispatchQueue.main.async { 22 | var mainWindow = NSWindow() 23 | mainWindow = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 812, height: 600), styleMask: .titled, backing: .buffered, defer: false) 24 | guard let viewModel = OnboardingViewModel(onboardingData, window: mainWindow, position: object.position, timeout: object.timeout) else { return } 25 | mainWindow.delegate = viewModel 26 | let contentView = OnboardingView(viewModel: viewModel) 27 | mainWindow.contentView = NSHostingView(rootView: contentView) 28 | mainWindow.setWindowPosition(object.position ?? .center) 29 | mainWindow.styleMask.remove(.resizable) 30 | if object.payload?.progressBarPayload != nil { 31 | mainWindow.styleMask.remove(.closable) 32 | mainWindow.styleMask.remove(.miniaturizable) 33 | } else if object.hideTitleBarButtons ?? false { 34 | mainWindow.styleMask.remove(.closable) 35 | mainWindow.styleMask.remove(.miniaturizable) 36 | } else { 37 | // Not sure why since Sonoma we need to manually add the default buttons to the Title Bar. 38 | mainWindow.styleMask.update(with: .closable) 39 | mainWindow.styleMask.update(with: .miniaturizable) 40 | } 41 | mainWindow.canBecomeVisibleWithoutLogin = true 42 | 43 | if object.forceLightMode ?? false { 44 | NSApp.appearance = NSAppearance(named: .aqua) 45 | } 46 | mainWindow.title = "" 47 | mainWindow.titlebarAppearsTransparent = true 48 | 49 | if let backgroundPanelStyle = object.backgroundPanel { 50 | mainWindow.level = .init(Int(CGWindowLevelForKey(.maximumWindow)) + 2) 51 | mainWindow.isMovable = false 52 | mainWindow.collectionBehavior = [.stationary, .canJoinAllSpaces] 53 | Context.main.backgroundPanelsController = BackPanelController(backgroundPanelStyle) 54 | Context.main.backgroundPanelsController?.showBackgroundWindows() 55 | } else { 56 | mainWindow.isMovable = object.isMovable 57 | mainWindow.level = object.alwaysOnTop ?? false ? .floating : .normal 58 | } 59 | 60 | mainWindow.makeKeyAndOrderFront(self) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Notification Agent Onboarding/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(TARGET_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | ITSAppUsesNonExemptEncryption 22 | 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSHumanReadableCopyright 35 | © Copyright IBM Corp. 2021, 2024 36 | NSMainStoryboardFile 37 | Main 38 | NSPrincipalClass 39 | NSApplication 40 | NSSupportsAutomaticTermination 41 | 42 | NSSupportsSuddenTermination 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Notification Agent Onboarding/Notification_Agent_Onboarding.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.disable-library-validation 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent Popup Tests/NAPTriggersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NAPTriggersTests.swift 3 | // Notification Agent Popups Tests 4 | // 5 | // Created by Simone Martorelli on 01/06/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import XCTest 11 | 12 | class NAPTriggersTests: XCTestCase { 13 | 14 | let worker = TestWorker() 15 | let parserUseCases = [["type" : "popup", "title" : "This is a title"], 16 | ["type" : "popup", "subtitle" : "This is a subtitle"], 17 | ["type" : "popup", "title" : "This is a title", "subtitle" : "This is a subtitle", "position" : "top_left"], 18 | ["type" : "popup", "title" : "This is a title", "subtitle" : "This is a subtitle", "silent" : "true"]] 19 | 20 | override func setUpWithError() throws { 21 | worker.startObservation() 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | worker.stopObservation() 26 | } 27 | 28 | func testArgumentParsing() throws { 29 | for useCase in parserUseCases { 30 | guard let notificationObject = try? NotificationObject(from: useCase), 31 | let settings = Context.main.sharedSettings else { 32 | XCTAssert(false, "Failed use case: \(useCase)") 33 | return 34 | } 35 | let taskObject = TaskObject(notification: notificationObject, settings: settings) 36 | guard let jsonData = try? JSONEncoder().encode(taskObject) else { 37 | XCTAssert(false, "Failed use case: \(useCase)") 38 | return 39 | } 40 | print("Case: \(jsonData.base64EncodedString())") 41 | EFCLController.shared.parseArguments([jsonData.base64EncodedString()]) 42 | XCTAssert(worker.argumentsSuccessfullyParsed, "Failed use case: \(useCase)") 43 | worker.argumentsSuccessfullyParsed = false 44 | } 45 | } 46 | } 47 | 48 | extension NAPTriggersTests { 49 | class TestWorker { 50 | var argumentsSuccessfullyParsed: Bool 51 | 52 | init() { 53 | argumentsSuccessfullyParsed = false 54 | } 55 | 56 | /// Adding notification observer 57 | func startObservation() { 58 | NotificationCenter.default.addObserver(self, 59 | selector: #selector(receivedNotification), 60 | name: .showNotification, 61 | object: nil) 62 | } 63 | 64 | /// Removing notification observer 65 | func stopObservation() { 66 | NotificationCenter.default.removeObserver(self, name: .showNotification, object: nil) 67 | } 68 | 69 | @objc func receivedNotification(_ notification: NSNotification) { 70 | argumentsSuccessfullyParsed = true 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Notification Agent Popups/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 18/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | import os.log 12 | 13 | @NSApplicationMain 14 | class AppDelegate: NSObject, NSApplicationDelegate { 15 | let notificationDispatch = NotificationDispatch.shared 16 | let efclController = EFCLController.shared 17 | let context = Context.main 18 | var isConfigured: Bool = false 19 | 20 | private func configureApp(_ completion: @escaping () -> Void = {}) { 21 | guard !isConfigured else { 22 | completion() 23 | return 24 | } 25 | isConfigured = true 26 | NSApplication.shared.activate(ignoringOtherApps: true) 27 | notificationDispatch.startObservingForNotifications() 28 | efclController.parseArguments() 29 | 30 | completion() 31 | } 32 | 33 | func applicationDidFinishLaunching(_ aNotification: Notification) { 34 | // Intercept the command+q shortcut and modify the exit value to reflect a manual user dismission. 35 | NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in 36 | switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) { 37 | case [.command] where event.characters == "q": 38 | guard !self.context.disableQuit else { return .none } 39 | Utils.applicationExit(withReason: .userDismissedPopup) 40 | default: 41 | return event 42 | } 43 | return event 44 | } 45 | configureApp() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_128x128.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "light" 13 | } 14 | ], 15 | "filename" : "icon_128x128 1.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "dark" 24 | } 25 | ], 26 | "filename" : "icon_128x128 2.png", 27 | "idiom" : "universal", 28 | "scale" : "1x" 29 | }, 30 | { 31 | "filename" : "icon_128x128@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "appearances" : [ 37 | { 38 | "appearance" : "luminosity", 39 | "value" : "light" 40 | } 41 | ], 42 | "filename" : "icon_128x128@2x 1.png", 43 | "idiom" : "universal", 44 | "scale" : "2x" 45 | }, 46 | { 47 | "appearances" : [ 48 | { 49 | "appearance" : "luminosity", 50 | "value" : "dark" 51 | } 52 | ], 53 | "filename" : "icon_128x128@2x 2.png", 54 | "idiom" : "universal", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "filename" : "icon_128x128@3x.png", 59 | "idiom" : "universal", 60 | "scale" : "3x" 61 | }, 62 | { 63 | "appearances" : [ 64 | { 65 | "appearance" : "luminosity", 66 | "value" : "light" 67 | } 68 | ], 69 | "filename" : "icon_128x128@3x 1.png", 70 | "idiom" : "universal", 71 | "scale" : "3x" 72 | }, 73 | { 74 | "appearances" : [ 75 | { 76 | "appearance" : "luminosity", 77 | "value" : "dark" 78 | } 79 | ], 80 | "filename" : "icon_128x128@3x 2.png", 81 | "idiom" : "universal", 82 | "scale" : "3x" 83 | } 84 | ], 85 | "info" : { 86 | "author" : "xcode", 87 | "version" : 1 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128 1.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128 2.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@2x 1.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@2x 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@2x 2.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@3x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@3x 1.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@3x 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@3x 2.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-notifications/2b49dbc1b4c7e01b2d59b598d17be0ed5fbbc6a8/Notification Agent Popups/Assets.xcassets/Common/default_icon.imageset/icon_128x128@3x.png -------------------------------------------------------------------------------- /Notification Agent Popups/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Notification Agent Popups/Controllers/PopupInteractiveEFCLController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopupInteractiveEFCLController.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 24/06/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | final class PopupInteractiveEFCLController: InteractiveEFCLController { 13 | override func processInput(_ notification: Notification) { 14 | let inputData = inputPipe.availableData 15 | if !inputData.isEmpty { 16 | guard let strData = String(data: inputData, encoding: String.Encoding.utf8)?.trimmingCharacters(in: CharacterSet.newlines) else { return } 17 | let splittedStrings = strData.split(separator: "/") 18 | for string in splittedStrings { 19 | guard let argument = string.split(separator: " ", maxSplits: 1).first?.lowercased(), 20 | var value = string.split(separator: " ", maxSplits: 1).last else { continue } 21 | if value.last == " " { 22 | value.removeLast() 23 | } 24 | switch argument { 25 | case "warning_button_visibility": 26 | NotificationCenter.default.post(name: Notification.Name("dynamic_button_updates"), object: nil, userInfo: ["data" : inputData]) 27 | case "percent", "top_message", "bottom_message", "user_interaction_enabled", "user_interruption_allowed", "exit_on_completion", "end": 28 | NotificationCenter.default.post(name: Notification.Name("progressbar_interactive_updates"), object: nil, userInfo: ["data" : inputData]) 29 | default: 30 | continue 31 | } 32 | } 33 | } 34 | inputPipe.waitForDataInBackgroundAndNotify() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Notification Agent Popups/Extensions/NotificationDispatch-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationDispatch-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 5/27/21. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Cocoa 12 | import SwiftUI 13 | 14 | extension NotificationDispatch { 15 | /// Handle the received notification and send the notification object to the correct controller. 16 | /// - Parameter notification: the received notification. 17 | @objc 18 | func receivedNotification(_ notification: Notification) { 19 | guard let object = notification.userInfo?["object"] as? NotificationObject else { return } 20 | switch object.type { 21 | case .systemalert: 22 | DispatchQueue.main.async { 23 | let alertController = SystemAlertController(object) 24 | alertController.showAlert() 25 | } 26 | case .popup: 27 | DispatchQueue.main.async { 28 | let windowWidth = CGFloat(truncating: NumberFormatter().number(from: object.customWidth ?? "520") ?? .init(integerLiteral: 520)) 29 | let mainWindow = NSWindow(contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: 130), styleMask: .titled, backing: .buffered, defer: false) 30 | let viewModel = PopUpViewModel(object, window: mainWindow) 31 | let contentView = PopUpView(viewModel: viewModel) 32 | let hostingView = NSHostingView(rootView: contentView) 33 | mainWindow.contentView = hostingView 34 | if object.hideTitleBar { 35 | mainWindow.title = "" 36 | mainWindow.titlebarAppearsTransparent = true 37 | } else { 38 | mainWindow.title = object.barTitle ?? ConfigurableParameters.defaultPopupBarTitle 39 | } 40 | mainWindow.setWindowPosition(object.position ?? .center) 41 | mainWindow.styleMask.remove(.resizable) 42 | mainWindow.styleMask.remove(.closable) 43 | mainWindow.canBecomeVisibleWithoutLogin = true 44 | mainWindow.setAccessibilityIdentifier("main_window") 45 | 46 | if let backgroundPanelStyle = object.backgroundPanel { 47 | mainWindow.level = .init(Int(CGWindowLevelForKey(.maximumWindow)) + 2) 48 | mainWindow.isMovable = false 49 | mainWindow.collectionBehavior = [.stationary, .canJoinAllSpaces] 50 | Context.main.backgroundPanelsController = BackPanelController(backgroundPanelStyle) 51 | Context.main.backgroundPanelsController?.showBackgroundWindows() 52 | } else { 53 | mainWindow.isMovable = object.isMovable 54 | mainWindow.level = object.alwaysOnTop ?? false ? .floating : .normal 55 | } 56 | 57 | if object.forceLightMode ?? false { 58 | NSApp.appearance = NSAppearance(named: .aqua) 59 | } 60 | if !(object.isMiniaturizable ?? false) { 61 | mainWindow.styleMask.remove(.miniaturizable) 62 | } 63 | 64 | mainWindow.makeKeyAndOrderFront(self) 65 | 66 | guard object.silent == false else { return } 67 | guard Utils.UISoundEffectStatusEnable else { return } 68 | NSSound(named: .init("Funk"))?.play() 69 | } 70 | default: 71 | Utils.applicationExit(withReason: .internalError) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Notification Agent Popups/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(TARGET_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | ITSAppUsesNonExemptEncryption 22 | 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSHumanReadableCopyright 35 | © Copyright IBM Corp. 2021, 2024 36 | NSMainStoryboardFile 37 | Main 38 | NSPrincipalClass 39 | NSApplication 40 | NSSupportsAutomaticTermination 41 | 42 | NSSupportsSuddenTermination 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Notification Agent Popups/Notification_Agent_Popups.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.disable-library-validation 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent Popups/Views/BodyLabels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BodyLabels.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 22/11/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | import SwiftyMarkdown 12 | 13 | /// BodyLabels is a struct that defines a view with the title and the subtitle for the PupUpView. 14 | struct BodyLabels: View { 15 | 16 | // MARK: - Environment Variables 17 | 18 | @EnvironmentObject var viewSpec: ViewSpec 19 | 20 | // MARK: - Variables 21 | 22 | var title: String? 23 | var titleFont: Font? 24 | var subtitle: String? 25 | 26 | // MARK: - Views 27 | 28 | var body: some View { 29 | VStack(alignment: .leading, spacing: 8) { 30 | if let title = title, !title.isEmpty { 31 | Text(title) 32 | .font(titleFont) 33 | .fixedSize(horizontal: false, vertical: true) 34 | .accessibilityIdentifier("popup_title") 35 | } 36 | if let subtitle = subtitle { 37 | MarkdownView(text: subtitle.localized, maxViewHeight: 450, containerWidth: viewSpec.accessoryViewWidth) 38 | .accessibilityElement() 39 | .accessibilityValue(SwiftyMarkdown(string: subtitle).attributedString().string) 40 | .accessibilityAddTraits(.isStaticText) 41 | .accessibilityIdentifier("popup_subtitle") 42 | } 43 | } 44 | } 45 | } 46 | 47 | struct BodyLabels_Previews: PreviewProvider { 48 | static var previews: some View { 49 | BodyLabels(title: "Some Title", subtitle: "Some Subtitle") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/xcshareddata/xcbaselines/F47F5552249B8C1C006A0754.xcbaseline/637884EE-58CB-47E9-AF0B-6447CD2D27AE.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | NotificationAgentUITests 8 | 9 | testLaunchPerformance() 10 | 11 | com.apple.dt.XCTMetric_OSSignpost-ApplicationLaunchExtended.duration 12 | 13 | baselineAverage 14 | 0.82116 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | PopupUITests 21 | 22 | testLaunchPerformance() 23 | 24 | com.apple.dt.XCTMetric_OSSignpost-ApplicationLaunchExtended.duration 25 | 26 | baselineAverage 27 | 0.82566 28 | baselineIntegrationDisplayName 29 | Local Baseline 30 | 31 | 32 | testPresentPopup() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 1.1156 38 | baselineIntegrationDisplayName 39 | Local Baseline 40 | 41 | com.apple.dt.XCTMetric_OSSignpost-ApplicationLaunchExtended.duration 42 | 43 | baselineAverage 44 | 0.82761 45 | baselineIntegrationDisplayName 46 | Local Baseline 47 | 48 | 49 | testPresentPopupPerformance() 50 | 51 | com.apple.XCTPerformanceMetric_WallClockTime 52 | 53 | baselineAverage 54 | 1.0981 55 | baselineIntegrationDisplayName 56 | Local Baseline 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/xcshareddata/xcbaselines/F47F5552249B8C1C006A0754.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 637884EE-58CB-47E9-AF0B-6447CD2D27AE 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 400 13 | cpuCount 14 | 1 15 | cpuKind 16 | 8-Core Intel Core i9 17 | cpuSpeedInMHz 18 | 2300 19 | logicalCPUCoresPerPackage 20 | 16 21 | modelCode 22 | MacBookPro15,1 23 | physicalCPUCoresPerPackage 24 | 8 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64h 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/xcshareddata/xcschemes/IBM Notifier Alert.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 47 | 49 | 55 | 56 | 57 | 58 | 64 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/xcshareddata/xcschemes/IBM Notifier Banner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 47 | 49 | 55 | 56 | 57 | 58 | 64 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/xcshareddata/xcschemes/IBM Notifier Onboarding.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 47 | 49 | 55 | 56 | 57 | 58 | 64 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Notification Agent.xcodeproj/xcshareddata/xcschemes/IBM Notifier Popup.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 47 | 49 | 55 | 56 | 57 | 58 | 64 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /NotificationAgentTP.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "7C58656A-37CF-4D8F-A85F-0955CCB89DF0", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "commandLineArgumentEntries" : [ 13 | { 14 | "argument" : "--isRunningTest" 15 | } 16 | ], 17 | "targetForVariableExpansion" : { 18 | "containerPath" : "container:Notification Agent.xcodeproj", 19 | "identifier" : "F47F5535249B8C1B006A0754", 20 | "name" : "IBM Notifier" 21 | }, 22 | "uiTestingScreenshotsLifetime" : "keepNever", 23 | "userAttachmentLifetime" : "keepAlways" 24 | }, 25 | "testTargets" : [ 26 | { 27 | "target" : { 28 | "containerPath" : "container:Notification Agent.xcodeproj", 29 | "identifier" : "FDFCA5372857CE3D009C1880", 30 | "name" : "Notification Agent Core Tests" 31 | } 32 | }, 33 | { 34 | "target" : { 35 | "containerPath" : "container:Notification Agent.xcodeproj", 36 | "identifier" : "FDFCA5762857D71F009C1880", 37 | "name" : "Notification Agent Alert Tests" 38 | } 39 | }, 40 | { 41 | "target" : { 42 | "containerPath" : "container:Notification Agent.xcodeproj", 43 | "identifier" : "FDFCA5832857D798009C1880", 44 | "name" : "Notification Agent Banner Tests" 45 | } 46 | }, 47 | { 48 | "target" : { 49 | "containerPath" : "container:Notification Agent.xcodeproj", 50 | "identifier" : "FDFCA5902857DC81009C1880", 51 | "name" : "Notification Agent Popup Tests" 52 | } 53 | }, 54 | { 55 | "target" : { 56 | "containerPath" : "container:Notification Agent.xcodeproj", 57 | "identifier" : "FDFCA5AC2857DD10009C1880", 58 | "name" : "Notification Agent Onboarding Tests" 59 | } 60 | }, 61 | { 62 | "target" : { 63 | "containerPath" : "container:Notification Agent.xcodeproj", 64 | "identifier" : "FDFCA59D2857DCC3009C1880", 65 | "name" : "Notification Agent Popup UI Tests" 66 | } 67 | }, 68 | { 69 | "target" : { 70 | "containerPath" : "container:Notification Agent.xcodeproj", 71 | "identifier" : "FDFCA5B92857DD62009C1880", 72 | "name" : "Notification Agent Onboarding UI Tests" 73 | } 74 | } 75 | ], 76 | "version" : 1 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IBM Notifier 2 | 3 | ![License](https://img.shields.io/badge/license-Apache%202-1984E5) 4 | 5 | ![Swift version](https://img.shields.io/badge/swift-5.9.0-1984E5?logo=swift) 6 | ![Project version](https://img.shields.io/badge/version-3.2.1-1984E5) 7 | ![macOS](https://img.shields.io/badge/macOS-11+-bright%20green) 8 | 9 | [![CI](https://github.com/IBM/mac-ibm-notifications/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/IBM/mac-ibm-notifications/actions/workflows/main.yml) 10 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5823/badge)](https://bestpractices.coreinfrastructure.org/projects/5823) 11 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=IBM_mac-ibm-notifications&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=IBM_mac-ibm-notifications) 12 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=IBM_mac-ibm-notifications&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=IBM_mac-ibm-notifications) 13 | 14 | ![GitHub Downloads (all assets, latest release)](https://img.shields.io/github/downloads/ibm/Mac-ibm-notifications/latest/total?logo=Github&label=Latest%20Release%20Downloads) 15 | ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/ibm/Mac-ibm-notifications/total?logo=Github&label=Total%20Release%20Downloads) 16 | 17 | ## Scope 18 | 19 | The purpose of this project is to provide an useful tool to system administrators to trigger interactive popups and notification banners to the end user. 20 | 21 | ## Usage 22 | 23 | To contribute to this repo please see [CONTRIBUTING.md](CONTRIBUTING.md) 24 | 25 | Here you can find the updated changelog [CHANGELOG.md](CHANGELOG.md) 26 | 27 | For detailed usage of this tool please refer to the [official wiki](https://github.com/IBM/mac-ibm-notifications/wiki/Usage) 28 | 29 | ## As seen on 30 | 31 | [JNUC 2021 Session 1167](https://www.youtube.com/watch?v=Cn5wIuB90t8&list=PLlxHm_Px-Ie1EIRlDHG2lW5H7c2UYvops&index=14) 32 | [JNUC 2021 Session 1171](https://www.youtube.com/watch?v=BOPAa8QZw0o&list=PLlxHm_Px-Ie1EIRlDHG2lW5H7c2UYvops&index=17) 33 | [JNUC 2022 Session 1174](https://www.youtube.com/watch?v=oTeJZnh2cZ0&list=PLlxHm_Px-Ie2uIFiar6_3JejiOnObiujM&index=107) 34 | [JNUC 2022 Session 1175](https://www.youtube.com/watch?v=9ZsZaSmWwIo&list=PLlxHm_Px-Ie2uIFiar6_3JejiOnObiujM&index=108) 35 | [JNUC 2023 Session 1327](https://youtu.be/aCkbOL2YYaw?si=rWGy4Cy2Qoki9Mm1) 36 | [JNUC 2023 Session 1168](https://youtu.be/Lj26nuCH1jA?si=I-tkL18rU-rLFlIz) 37 | 38 | ## Notes 39 | 40 | If you have any questions or issues you can create a new issue [here](https://github.com/IBM/mac-ibm-notifications/issues/new/choose). 41 | 42 | You can also find a list of the maintainers of this repo [here](MAINTAINERS.md). 43 | 44 | Don't forget to join the community in the `#mac-ibm-open-source` channel in the [MacAdmins Slack Workspace](https://www.macadmins.org). 45 | 46 | Pull requests are very welcome! Make sure your patches are well tested. 47 | Ideally create a topic branch for every separate change you make. For 48 | example: 49 | 50 | 1. Fork the repo 51 | 2. Create your feature branch (`git checkout -b my-new-feature`) 52 | 3. Commit your changes (`git commit -am 'Added some feature'`) 53 | 4. Push to the branch (`git push origin my-new-feature`) 54 | 5. Create new Pull Request 55 | 56 | ## License 57 | 58 | All source files must include a Copyright and License header. The SPDX license header is 59 | preferred because it can be easily scanned. 60 | 61 | If you would like to see the detailed LICENSE click [here](LICENSE). 62 | 63 | ```text 64 | # 65 | # Copyright 2020- IBM Inc. All rights reserved 66 | # SPDX-License-Identifier: Apache2.0 67 | # 68 | ``` 69 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ---------- | -------------------------- | 7 | | 3.1.0 | :white_check_mark: | 8 | | 3.0.3 | :x: | 9 | | 3.0.2 | :x: | 10 | | 3.0.0 | :x: | 11 | | 2.9.1 | :white_check_mark: | 12 | | < 2.9.1 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | To report a vulnerability, please e-mail to [maintainers](/MAINTAINERS.md) of this project with a description of the issue, 17 | the steps you took to create the issue, affected versions, and if known, mitigations for the issue. 18 | 19 | We should reply within three working days, probably much sooner. 20 | 21 | We use GitHub's security advisory feature to track open security issues. You should expect 22 | a close collaboration as we work to resolve the issue you have reported. 23 | 24 | You may also reach out to the team via [Github Discussions](https://github.com/IBM/mac-ibm-notifications/discussions) 25 | -------------------------------------------------------------------------------- /Shared/Controllers/BackPanelController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackPanelController.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 23/05/2023. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import AppKit 11 | 12 | /// BackPanelController handle the background panel appearance. 13 | /// It detects screen seetting changes and react to provide a better UX. 14 | final class BackPanelController { 15 | 16 | // MARK: - Variables 17 | 18 | private var backWindows: [BackPanelWindow] 19 | private var backgroundPanelStyle: NotificationObject.BackgroundPanelStyle 20 | 21 | // MARK: - Initializers 22 | 23 | init(_ backgroundPanelStyle: NotificationObject.BackgroundPanelStyle) { 24 | self.backWindows = [] 25 | self.backgroundPanelStyle = backgroundPanelStyle 26 | NotificationCenter.default.addObserver(self, selector: #selector(screenSettingsDidChange), name: NSApplication.didChangeScreenParametersNotification, object: nil) 27 | } 28 | 29 | // MARK: - Public Methods 30 | 31 | /// Shows the background panels on all displays and spaces. 32 | func showBackgroundWindows() { 33 | for screen in NSScreen.screens { 34 | let backWindow = BackPanelWindow(screen, backgroundPanelStyle) 35 | backWindow.level = .init(Int(CGWindowLevelForKey(.maximumWindow) + 1)) 36 | backWindow.makeKeyAndOrderFront(nil) 37 | backWindows.append(backWindow) 38 | } 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | @objc 44 | private func screenSettingsDidChange() { 45 | var newWindows: [BackPanelWindow] = [] 46 | for screen in NSScreen.screens { 47 | guard backWindows.map({ $0.screen }).contains(where: { $0 == screen }) else { 48 | let backWindow = BackPanelWindow(screen, backgroundPanelStyle) 49 | backWindow.level = .init(Int(CGWindowLevelForKey(.maximumWindow) + 1)) 50 | backWindow.makeKeyAndOrderFront(nil) 51 | newWindows.append(backWindow) 52 | continue 53 | } 54 | } 55 | backWindows.append(contentsOf: newWindows) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Shared/Controllers/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Context.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 31/03/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Application context 13 | final class Context { 14 | 15 | static let main: Context = Context() 16 | 17 | // MARK: - Variables 18 | 19 | private var _sharedSettings: SharedSettings? { 20 | didSet { 21 | guard let newSettings = _sharedSettings else { return } 22 | UserDefaults.standard.set(newSettings.environment.rawValue, forKey: "environment") 23 | } 24 | } 25 | var sharedSettings: SharedSettings? { 26 | get { 27 | guard let settings = _sharedSettings else { 28 | return SharedSettings(isVerboseModeEnabled: false, 29 | environment: Environment.current) 30 | } 31 | return settings 32 | } 33 | set { 34 | _sharedSettings = newValue 35 | } 36 | } 37 | var backgroundPanelsController: BackPanelController? 38 | var disableQuit: Bool = false 39 | } 40 | -------------------------------------------------------------------------------- /Shared/Controllers/EFCLController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFCLController.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 8/26/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// ExecutionFromCommandLineController handle the launch of the agent from command line. 13 | class EFCLController { 14 | 15 | // MARK: - Static Variables 16 | 17 | static let shared = EFCLController() 18 | static let specialArguments = ["-NSDocumentRevisionsDebugMode", 19 | "--isRunningTest", 20 | "--v", 21 | "--help", 22 | "--version", 23 | "--terms", 24 | "--privacy", 25 | "--isRunningTest", 26 | "--config", 27 | "--resetBanners", 28 | "--resetAlerts", 29 | "-reset", 30 | "sudo"] 31 | static let standaloneBooleanArguments = ["always_on_top", 32 | "silent", 33 | "miniaturizable", 34 | "force_light_mode", 35 | "hide_title_bar_buttons", 36 | "retain_values", 37 | "show_suppression_button", 38 | "unmovable", 39 | "disable_quit", 40 | "buttonless", 41 | "hide_title_bar"] 42 | // MARK: - Variables 43 | 44 | let context = Context.main 45 | let logger = NALogger.shared 46 | var isRunningTestForEFCL: Bool { 47 | return CommandLine.arguments.contains("--isRunningTest") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Shared/Controllers/Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 28/04/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | struct Constants { 13 | static internal var environmentUDKey: String = "environment" 14 | static internal var loginItemEnabledUDKey = "loginItemEnabled" 15 | static internal let storeFileName = "IBM_Notifier_Onboarding.plist" 16 | } 17 | 18 | struct AppVersion: Comparable, Equatable { 19 | let major: Int 20 | let release: Int 21 | let fix: Int 22 | 23 | var current: AppVersion? { 24 | guard let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { return nil } 25 | let components = appVersion.split(separator: ".") 26 | guard components.count == 3, 27 | let major = Int(components[0]), 28 | let release = Int(components[1]), 29 | let fix = Int(components[2]) else { return nil } 30 | return AppVersion(major: major, release: release, fix: fix) 31 | } 32 | 33 | func isFinalDeprecatedVersion() -> Bool { 34 | guard let currentVersion = self.current else { 35 | return false 36 | } 37 | return currentVersion >= self 38 | } 39 | 40 | static func < (lhs: AppVersion, rhs: AppVersion) -> Bool { 41 | if lhs.major < rhs.major { 42 | return true 43 | } else if lhs.major == rhs.major { 44 | return lhs.release < rhs.release 45 | } else { 46 | return false 47 | } 48 | } 49 | } 50 | 51 | // swiftlint:disable identifier_name 52 | 53 | enum Environment: String, Codable { 54 | case eng 55 | case qa 56 | case prod 57 | 58 | static var current: Environment { 59 | if let environmentRawValue = UserDefaults.standard.string(forKey: Constants.environmentUDKey), 60 | let environment = Environment(rawValue: environmentRawValue.lowercased()) { 61 | return environment 62 | } else { 63 | return prod 64 | } 65 | } 66 | } 67 | 68 | // swiftlint:enable identifier_name 69 | -------------------------------------------------------------------------------- /Shared/Controllers/InteractiveEFCLContoller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InteractiveEFCLContoller.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 10/15/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This controller handle the background observation for interactive input from command line execution of the agent. 13 | open class InteractiveEFCLController { 14 | 15 | // MARK: Variables 16 | var subtask: Process! 17 | var inputPipe: FileHandle! 18 | 19 | // MARK: - Initializers 20 | 21 | init(for subtask: Process? = nil) { 22 | self.subtask = subtask 23 | } 24 | 25 | deinit { 26 | NotificationCenter.default.removeObserver(self, name: .NSFileHandleDataAvailable, object: nil) 27 | } 28 | 29 | // MARK: - Public methods 30 | 31 | /// Start the background observing for new inputs. 32 | func startObservingStandardInput() { 33 | inputPipe = FileHandle.standardInput 34 | NotificationCenter.default.addObserver(forName: .NSFileHandleDataAvailable, object: nil, queue: .main, using: self.processInput(_:)) 35 | inputPipe.waitForDataInBackgroundAndNotify() 36 | } 37 | 38 | /// Process the new data from the standard input 39 | func processInput(_ notification: Notification) { 40 | let inputData = inputPipe.availableData 41 | if !inputData.isEmpty { 42 | guard let strData = String(data: inputData, encoding: String.Encoding.utf8), 43 | strData.trimmingCharacters(in: CharacterSet.newlines).lowercased() != "exit" else { 44 | self.subtask.interrupt() 45 | exit(0) 46 | } 47 | (self.subtask.standardInput as? Pipe)?.fileHandleForWriting.write(inputData) 48 | } 49 | inputPipe.waitForDataInBackgroundAndNotify() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Shared/Controllers/NALogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 8/27/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import os.log 12 | 13 | /// A simple class based on Apple os.log that handle normal and verbose logs. 14 | public final class NALogger { 15 | 16 | // MARK: - Static Variables 17 | 18 | static let shared = NALogger() 19 | 20 | // MARK: - Methods 21 | 22 | func log(_ type: OSLogType, _ message: StaticString, _ args: [String] = []) { 23 | Logger().log(level: type, 24 | "\(String(format: message.description.replacingOccurrences(of: "{public}", with: ""), arguments: args), privacy: .public)") 25 | if (Context.main.sharedSettings?.isVerboseModeEnabled ?? false) || type == .error { 26 | self.verbose(type, message, args) 27 | } 28 | } 29 | func log(_ message: StaticString, _ args: [String] = []) { 30 | Logger().log(level: .default, 31 | "\(String(format: message.description.replacingOccurrences(of: "{public}", with: ""), arguments: args), privacy: .public)") 32 | if Context.main.sharedSettings?.isVerboseModeEnabled ?? false { 33 | self.verbose(.default, message, args) 34 | } 35 | } 36 | func deprecationLog(since version: AppVersion, deprecatedArgument: String) { 37 | if version.isFinalDeprecatedVersion() { 38 | self.log(.error, "The following argument has been deprecated: %{public}@. Please update your workflow.", [deprecatedArgument]) 39 | } else { 40 | self.log("The following argument has been deprecated and will not be supported anymore soon: %{public}@. Make sure to update your workflow as soon as possible.", [deprecatedArgument]) 41 | } 42 | } 43 | 44 | // MARK: - Private Methods 45 | 46 | private func verbose(_ type: OSLogType, _ message: StaticString, _ args: [String] = []) { 47 | let message = type == .error ? 48 | message.description.replacingOccurrences(of: "{public}", with: "").red() : 49 | message.description.replacingOccurrences(of: "{public}", with: "").yellow() 50 | 51 | print(String(format: message, arguments: args)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Shared/Controllers/NotificationDispatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationDispatch.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 7/29/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Cocoa 12 | import SystemConfiguration 13 | 14 | /// Dispatch to the right controller the received notification object. 15 | final class NotificationDispatch { 16 | 17 | // MARK: - Static constants 18 | 19 | static let shared = NotificationDispatch() 20 | 21 | // MARK: - Variables 22 | 23 | var taskManager = TaskManager() 24 | 25 | // MARK: - Methods 26 | 27 | /// Start observing for notifications triggered by other controllers. 28 | func startObservingForNotifications() { 29 | NotificationCenter.default.addObserver(self, 30 | selector: #selector(receivedNotification), 31 | name: .showNotification, 32 | object: nil) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Shared/Controllers/ReplyHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplyHandler.swift 3 | // Notification Agent 4 | // 5 | // Created by Jan Valentik on 25/08/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Cocoa 12 | import os.log 13 | import UserNotifications 14 | 15 | /// This class will handle the user response to the showed UI. 16 | public final class ReplyHandler { 17 | 18 | // MARK: - Static variables 19 | 20 | static let shared = ReplyHandler() 21 | 22 | // MARK: - Variables 23 | 24 | let efclController = EFCLController.shared 25 | let context = Context.main 26 | 27 | // MARK: - Public methods 28 | 29 | /// Handle the response based on the type and on the showed NotificationObject. 30 | /// - Parameters: 31 | /// - type: the type of the user response. 32 | /// - object: the notification showed to the user. 33 | func handleResponse(ofType type: UserReplyType, for object: NotificationObject) { 34 | var triggerButton: NotificationButton? 35 | var exitReason: Utils.ExitReason? 36 | 37 | switch type { 38 | case .main: 39 | triggerButton = object.mainButton 40 | exitReason = .mainButtonClicked 41 | case .secondary: 42 | triggerButton = object.secondaryButton 43 | exitReason = .secondaryButtonClicked 44 | case .tertiary: 45 | triggerButton = object.tertiaryButton 46 | if object.type == .banner || object.tertiaryButton?.callToActionType == .exitlink { 47 | exitReason = .tertiaryButtonClicked 48 | } 49 | case .help: 50 | triggerButton = object.helpButton 51 | case .warning: 52 | triggerButton = object.warningButton 53 | case .dismiss: 54 | Utils.applicationExit(withReason: .userDismissedNotification) 55 | case .cancel: 56 | Utils.applicationExit(withReason: .cancelPressed) 57 | case .timeout: 58 | Utils.applicationExit(withReason: .timeout) 59 | } 60 | 61 | guard let button = triggerButton else { return } 62 | 63 | switch button.callToActionType { 64 | case .link, .exitlink: 65 | self.open(button.callToActionPayload) 66 | fallthrough 67 | default: 68 | guard let reason = exitReason else { return } 69 | Utils.applicationExit(withReason: reason) 70 | } 71 | } 72 | 73 | // MARK: - Private methods 74 | 75 | private func open(_ link: String) { 76 | guard let url = URL(string: link) else { 77 | NALogger.shared.log("Failed to create a valid URL or App path from payload: %{public}@", [link]) 78 | return 79 | } 80 | if ProcessInfo.processInfo.environment["--isRunningTest"] == nil { 81 | NSWorkspace.shared.open(url) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Shared/Controllers/TaskManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskManager.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 16/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import SystemConfiguration 12 | 13 | final class TaskManager { 14 | func runSyncTaskOnComponent(_ component: AppComponent, with jsonObject: Data, completion: (Int32) -> Void) -> Int32 { 15 | let task = buildTask(for: component, with: jsonObject) 16 | task.launch() 17 | task.waitUntilExit() 18 | if let outputData = (task.standardOutput as? Pipe)?.fileHandleForReading.availableData, 19 | let output = String(data: outputData, encoding: .utf8) { 20 | NALogger.shared.log("%{public}@", [output]) 21 | } 22 | if let errorData = (task.standardError as? Pipe)?.fileHandleForReading.availableData, 23 | let error = String(data: errorData, encoding: .utf8) { 24 | NALogger.shared.log("%{public}@", [error]) 25 | } 26 | return task.terminationStatus 27 | } 28 | 29 | func runAsyncTaskOnComponent(_ component: AppComponent, with jsonObject: Data, completion: @escaping (Int32) -> Void) { 30 | let task = buildTask(for: component, with: jsonObject) 31 | task.launch() 32 | task.waitUntilExit() 33 | if let outputData = (task.standardOutput as? Pipe)?.fileHandleForReading.availableData, 34 | let output = String(data: outputData, encoding: .utf8) { 35 | NALogger.shared.log("%{public}@", [output]) 36 | } 37 | if let errorData = (task.standardError as? Pipe)?.fileHandleForReading.availableData, 38 | let error = String(data: errorData, encoding: .utf8) { 39 | NALogger.shared.log("%{public}@", [error]) 40 | } 41 | completion(task.terminationStatus) 42 | } 43 | 44 | func runUntrackedTaskOnComponent(_ component: AppComponent, with jsonObject: Data) { 45 | let task = buildTask(for: component, with: jsonObject) 46 | task.launch() 47 | } 48 | 49 | func buildTask(for component: AppComponent, with jsonObject: Data) -> Process { 50 | let task = Process() 51 | let outputPipe = Pipe() 52 | let errorPipe = Pipe() 53 | task.standardOutput = outputPipe 54 | task.standardError = errorPipe 55 | if let loggedInUser = loggedUser() { 56 | let suArgsString = "'" + component.getRelativeComponentPath() + "'" + " " + jsonObject.base64EncodedString() 57 | var suArgsArray: [String] = [suArgsString] 58 | task.launchPath = "/usr/bin/su" 59 | suArgsArray.insert("-c", at: 0) 60 | suArgsArray.insert(loggedInUser, at: 0) 61 | suArgsArray.insert("-l", at: 0) 62 | task.arguments = suArgsArray 63 | } else { 64 | task.launchPath = component.getRelativeComponentPath() 65 | task.arguments = [jsonObject.base64EncodedString()] 66 | } 67 | return task 68 | } 69 | 70 | func loggedUser() -> String? { 71 | guard let loggedInUser = SCDynamicStoreCopyConsoleUser(nil, nil, nil) as String?, 72 | !loggedInUser.isEmpty && loggedInUser != "loginwindow" else { return nil } 73 | let userName = NSUserName() 74 | if userName != loggedInUser { 75 | return loggedInUser 76 | } else { 77 | return nil 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Shared/Extensions/Binding-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 11/01/23. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | extension Binding { 13 | 14 | /// When the `Binding`'s `wrappedValue` changes, the given closure is executed. 15 | /// - Parameter closure: Chunk of code to execute whenever the value changes. 16 | /// - Returns: New `Binding`. 17 | func onUpdate(_ closure: @escaping () -> Void) -> Binding { 18 | Binding(get: { 19 | wrappedValue 20 | }, set: { newValue in 21 | wrappedValue = newValue 22 | closure() 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Shared/Extensions/Collection-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 10/05/2023. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Collection where Indices.Iterator.Element == Index { 13 | subscript (safe index: Index) -> Iterator.Element? { 14 | return indices.contains(index) ? self[index] : nil 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Shared/Extensions/Data-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 06/12/2023. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Data { 13 | /// Boolean value that tells if the current data represent a GIF. 14 | /// Use this only if you're sure that the data represent an image. 15 | var isGIF: Bool { 16 | guard self.count > 3 else { 17 | return false 18 | } 19 | let gifSignature = "GIF".data(using: .ascii) 20 | return self.prefix(3) == gifSignature 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Shared/Extensions/Decodable-Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decodable-Extensions.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 22/01/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Decodable { 13 | /// Initializing object decoding JSON format string. 14 | /// - Parameter json: the JSON format string. 15 | /// - Throws: deconding or data errors. 16 | init(from json: String) throws { 17 | guard let jsonData = json.data(using: .utf8) else { 18 | throw NAError.dataFormat(type: .invalidJSONPayload) 19 | } 20 | do { 21 | self = try JSONDecoder().decode(Self.self, from: jsonData) 22 | } catch let error { 23 | throw NAError.dataFormat(type: .invalidJSONDecoding(errorDescription: error.localizedDescription)) 24 | } 25 | } 26 | /// Intializing obeject from decoding JSON file. 27 | /// - Parameter url: JSON file url. 28 | /// - Throws: deconding or url errors. 29 | init(from url: URL) throws { 30 | guard let jsonData = try? Data(contentsOf: url, options: .mappedIfSafe) else { 31 | throw NAError.dataFormat(type: .invalidJSONFilepath) 32 | } 33 | do { 34 | self = try JSONDecoder().decode(Self.self, from: jsonData) 35 | } catch let error { 36 | throw NAError.dataFormat(type: .invalidJSONDecoding(errorDescription: error.localizedDescription)) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Shared/Extensions/EFCLController-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFCLController-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 8/26/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension EFCLController { 13 | 14 | // MARK: - Methods 15 | 16 | /// Check if the app is running testes for other workflows, debug or different workflow and if not start parsing the launch arguments. 17 | func parseArguments(_ arguments: [String] = CommandLine.arguments) { 18 | guard !arguments.contains("--isRunningTest") else { return } 19 | do { 20 | guard let base64EncodedData = arguments.last, 21 | let data = Data(base64Encoded: base64EncodedData) else { 22 | Utils.applicationExit(withReason: .internalError) 23 | return 24 | } 25 | let taskObject = try JSONDecoder().decode(TaskObject.self, from: data) 26 | context.sharedSettings = taskObject.settings 27 | context.disableQuit = taskObject.notification.disableQuit 28 | NotificationCenter.default.post(name: .showNotification, 29 | object: self, 30 | userInfo: ["object": taskObject.notification]) 31 | } catch let error { 32 | guard let efclError = error as? NAError else { 33 | logger.log("No recognized error: %{public}@.", [error.localizedDescription]) 34 | Utils.applicationExit(withReason: .internalError) 35 | return 36 | } 37 | logger.log("%{public}@", [efclError.localizedDescription]) 38 | Utils.applicationExit(withReason: efclError.efclExitReason) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Shared/Extensions/Int-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 8/10/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Int { 13 | var timeFormattedString: String { 14 | let seconds = self % 60 15 | let minutes = (self / 60) % 60 16 | let hours = (self / 3600) 17 | 18 | return String(format: "%0.2d:%0.2d:%0.2d", hours, minutes, seconds) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Extensions/NSColor-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSColor-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 15/04/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | 12 | extension NSColor { 13 | var hexString: String { 14 | guard let rgbColor = usingColorSpace(NSColorSpace.deviceRGB) else { 15 | return "#FFFFFF" 16 | } 17 | let red = Int(round(rgbColor.redComponent * 0xFF)) 18 | let green = Int(round(rgbColor.greenComponent * 0xFF)) 19 | let blue = Int(round(rgbColor.blueComponent * 0xFF)) 20 | let hexString = NSString(format: "#%02X%02X%02X", red, green, blue) 21 | return hexString as String 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Shared/Extensions/NSScreen-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSWindow-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 27/04/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | 12 | extension NSWindow { 13 | enum WindowPosition: String, Codable { 14 | case topLeft = "top_left" 15 | case topRight = "top_right" 16 | case bottomLeft = "bottom_left" 17 | case bottomRight = "bottom_right" 18 | case center = "center" 19 | } 20 | 21 | func setWindowPosition(_ position: WindowPosition) { 22 | guard let mainScreen = NSScreen.main else { 23 | self.center() 24 | return 25 | } 26 | let minX = mainScreen.visibleFrame.minX + 30 27 | let minY = mainScreen.visibleFrame.minY + 30 + self.frame.size.height 28 | let maxX = mainScreen.visibleFrame.maxX - 30 - self.frame.size.width 29 | let maxY = mainScreen.visibleFrame.maxY - 30 30 | switch position { 31 | case .topRight: 32 | self.setFrameTopLeftPoint(CGPoint(x: maxX, y: maxY)) 33 | case .topLeft: 34 | self.setFrameTopLeftPoint(CGPoint(x: minX, y: maxY)) 35 | case .bottomLeft: 36 | self.setFrameTopLeftPoint(CGPoint(x: minX, y: minY)) 37 | case .bottomRight: 38 | self.setFrameTopLeftPoint(CGPoint(x: maxX, y: minY)) 39 | case .center: 40 | self.center() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Shared/Extensions/Notification-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 7/10/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Notification.Name { 13 | static let showNotification = Notification.Name(rawValue: "showNotification") 14 | static let onboardingParentStatusDidChange = Notification.Name(rawValue: "onboardingParentStatusDidChange") 15 | } 16 | -------------------------------------------------------------------------------- /Shared/Extensions/UNNotificationAttachment-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UNNotificationAttachment-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 03/12/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import UserNotifications 11 | 12 | extension UNNotificationAttachment { 13 | static func create(imageFileIdentifier: String, data: NSData, options: [NSObject : AnyObject]?) throws -> UNNotificationAttachment? { 14 | let fileManager = FileManager.default 15 | let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString 16 | guard let tmpSubFolderURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true) else { return nil } 17 | try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil) 18 | let fileURL = tmpSubFolderURL.appendingPathComponent(imageFileIdentifier) 19 | try data.write(to: fileURL, options: []) 20 | let imageAttachment = try UNNotificationAttachment(identifier: imageFileIdentifier, url: fileURL, options: options) 21 | return imageAttachment 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Shared/Extensions/View-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View-Extension.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 06/02/23. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | extension View { 13 | func compatibleAccessibilityLabel(label: String) -> some View { 14 | return self.accessibilityLabel(label) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Shared/Model/Common/ACVDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACVDecoder.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 15/12/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | protocol ACVDecodable { 13 | init(stringLiteral: String) 14 | } 15 | 16 | protocol AVCIterable: CaseIterable, CodingKey {} 17 | 18 | struct ACVDecoder { 19 | 20 | var codingKeys: any Collection 21 | 22 | // swiftlint:disable force_cast 23 | 24 | init(codingKeys: T.Type) where T : AVCIterable { 25 | self.codingKeys = codingKeys.allCases as! [any CodingKey] 26 | } 27 | 28 | // swiftlint:enable force_cast 29 | 30 | // MARK: - Private Methods 31 | 32 | func decode(key: CodingKey, ofType type: T.Type, from payload: String) throws -> T where T : ACVDecodable { 33 | guard codingKeys.contains(where: { $0.stringValue == key.stringValue }) else { 34 | throw NAError.efclController(type: .invalidAccessoryViewPayload) 35 | } 36 | 37 | var splittedStrings = payload.replacingOccurrences(of: "//", with: "/#escaping-double-slash /").split(separator: "/") 38 | guard splittedStrings.count > 0 else { throw NAError.efclController(type: .invalidAccessoryViewPayload) } 39 | splittedStrings.reverse() 40 | 41 | for index in 0.. String { 62 | guard Self.current != .core else { 63 | return Bundle.main.bundlePath + self.componentDirectory + self.bundleName + ".app" + self.binaryPath 64 | } 65 | return Bundle.main.bundlePath.replacingOccurrences(of: "\(Self.current.bundleName)", with: "\(self.bundleName)") + self.binaryPath 66 | } 67 | func cleanSavedFiles() { 68 | switch self { 69 | case .onboarding: 70 | Utils.delete(Constants.storeFileName) 71 | case .popup, .alert, .banner, .core: 72 | break 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Shared/Model/Common/Claims.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Claims.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 18/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import SwiftJWT 12 | 13 | struct MacAtIbmClaims: Claims { 14 | let sub: String 15 | let iss: String 16 | let exp: Date? 17 | } 18 | -------------------------------------------------------------------------------- /Shared/Model/Common/InfoSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfoSection.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 8/18/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// A single info field 13 | struct InfoField: Codable, Identifiable { 14 | var id: UUID 15 | var label: String 16 | var description: String? 17 | var iconName: String? 18 | 19 | init(label: String, description: String? = nil, iconName: String? = nil) { 20 | self.id = UUID() 21 | self.label = label 22 | self.description = description 23 | self.iconName = iconName 24 | } 25 | 26 | enum CodingKeys: CodingKey { 27 | case id 28 | case label 29 | case description 30 | case iconName 31 | } 32 | 33 | func encode(to encoder: Encoder) throws { 34 | var container = encoder.container(keyedBy: CodingKeys.self) 35 | try container.encode(self.id, forKey: .id) 36 | try container.encode(self.label, forKey: .label) 37 | try container.encodeIfPresent(self.description, forKey: .description) 38 | try container.encodeIfPresent(self.iconName, forKey: .iconName) 39 | } 40 | 41 | init(from decoder: Decoder) throws { 42 | let container = try decoder.container(keyedBy: CodingKeys.self) 43 | self.label = try container.decode(String.self, forKey: .label) 44 | self.description = try container.decodeIfPresent(String.self, forKey: .description) 45 | self.id = UUID() 46 | self.iconName = try container.decodeIfPresent(String.self, forKey: .iconName) 47 | } 48 | } 49 | 50 | /// Thi object reprensent an info section with multiple info fields. 51 | struct InfoSection: Codable { 52 | var fields: [InfoField] 53 | } 54 | -------------------------------------------------------------------------------- /Shared/Model/Common/LoadableNib.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadableNib.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 3/5/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | 12 | protocol LoadableNib { 13 | var contentView: NSView! { get } 14 | } 15 | 16 | extension LoadableNib where Self: NSView { 17 | func loadViewFromNib() { 18 | let bundle = Bundle(for: type(of: self)) 19 | let nib = NSNib(nibNamed: .init(String(describing: type(of: self))), bundle: bundle)! 20 | _ = nib.instantiate(withOwner: self, topLevelObjects: nil) 21 | 22 | let contentConstraints = contentView.constraints 23 | contentView.subviews.forEach({ addSubview($0) }) 24 | 25 | for constraint in contentConstraints { 26 | let firstItem = (constraint.firstItem as? NSView == contentView) ? self : constraint.firstItem 27 | let secondItem = (constraint.secondItem as? NSView == contentView) ? self : constraint.secondItem 28 | addConstraint(NSLayoutConstraint(item: firstItem as Any, 29 | attribute: constraint.firstAttribute, 30 | relatedBy: constraint.relation, 31 | toItem: secondItem, 32 | attribute: constraint.secondAttribute, 33 | multiplier: constraint.multiplier, 34 | constant: constraint.constant)) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Shared/Model/Common/PickerItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PickerItem.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 15/12/22. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | struct PickerItem: Identifiable, Hashable { 13 | var id: Int 14 | var label: String 15 | var isSelected: Bool 16 | 17 | init(index: Int, label: String, isSelected: Bool) { 18 | self.id = index 19 | self.label = label 20 | self.isSelected = isSelected 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Shared/Model/Common/SharedSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedSettings.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 22/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This object represent the shared settings between the app components and the core service. 13 | struct SharedSettings: Codable { 14 | var isVerboseModeEnabled: Bool 15 | var environment: Environment 16 | } 17 | -------------------------------------------------------------------------------- /Shared/Model/Common/TaskObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskObject.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 22/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This object represent a task sent from the core service to one of the app components. 13 | struct TaskObject: Codable { 14 | var notification: NotificationObject 15 | var settings: SharedSettings 16 | } 17 | -------------------------------------------------------------------------------- /Shared/Model/Common/Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Token.swift 3 | // Notification Agent 4 | // 5 | // Created by Jan Valentik on 20/11/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This object represent a received auth Token. 13 | internal struct Token: Codable { 14 | var value: String 15 | var expiration: Date? 16 | var isExpired: Bool { 17 | guard let exp = expiration else { 18 | return true 19 | } 20 | return exp < Date() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Shared/Model/Common/UserReplyType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserReplyType.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 17/06/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// All the different kind of response that the UI could receive from the user. 13 | enum UserReplyType: String { 14 | case main // Click on the main button (or on the notification banner for "banner" UI type). 15 | case secondary // Click on the secondary button. 16 | case tertiary // Click on the tertiary button. 17 | case help // Click on the help button. Help button type "infoPopup" is managed in the viewController itself. 18 | case warning // Click on the warning button. Warning button type "infoPopup" is managed in the viewController itself. 19 | case dismiss // "banner" UI type UI dismissed. 20 | case cancel // "Cancel" button pressed on popup. 21 | case timeout // Timeout. 22 | } 23 | -------------------------------------------------------------------------------- /Shared/Model/UIObjects/ConfigurableParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurableParameter.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 9/10/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This class provide all the configurable variables available for the agent. 13 | public final class ConfigurableParameters { 14 | static let set = ["default_popup_bar_title": "NADefaultPopupBarTitle", 15 | "default_popup_icon_path": "NADefaultPopupIconPath", 16 | "default_popup_timeout": "NADefaultPopupTimeout", 17 | "default_main_button_label": "NADefaultMainButtonLabel"] 18 | /// The default main button label. 19 | /// By default this string is defined in the Localizable.strings files. 20 | static var defaultMainButtonLabel: String { 21 | if let userDefaultsValue = UserDefaults.standard.string(forKey: "NADefaultMainButtonLabel"), 22 | !userDefaultsValue.isEmpty { 23 | return userDefaultsValue 24 | } 25 | return "default_main_button_label".localized 26 | } 27 | /// The default timeout for the popup in seconds. 28 | /// By default no timeout is set. 29 | static var defaultPopupTimeout: Int? { 30 | if let timeoutString = UserDefaults.standard.string(forKey: "NADefaultPopupTimeout") { 31 | return Int(timeoutString) 32 | } 33 | return nil 34 | } 35 | /// The default bar title for the popup. 36 | /// By default this string is defined in the Localizable.strings files. 37 | static var defaultPopupBarTitle: String { 38 | if let userDefaultsValue = UserDefaults.standard.string(forKey: "NADefaultPopupBarTitle"), 39 | !userDefaultsValue.isEmpty { 40 | return userDefaultsValue 41 | } 42 | return "default_popup_bar_title".localized 43 | } 44 | /// The default path for the popup icon. 45 | /// By default the agent take it from it's assets. 46 | static var defaultPopupIconPath: String? { 47 | if let defaultPath = UserDefaults.standard.string(forKey: "NADefaultPopupIconPath"), 48 | !defaultPath.isEmpty { 49 | return defaultPath 50 | } 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Shared/Model/UIObjects/OnboardingData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingData.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 21/01/2021. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Cocoa 12 | import AVFoundation 13 | 14 | /// This object describe the data defined for the onboarding. 15 | public final class OnboardingData: Codable { 16 | /// An array of pages. 17 | var pages: [InteractiveOnboardingPage] 18 | var progressBarPayload: String? 19 | var outputFile: String! 20 | 21 | // MARK: - Codable protocol conformity - START 22 | 23 | enum ODCodingKeys: String, CodingKey { 24 | case pages 25 | case progressBarPayload 26 | case outputFile 27 | } 28 | 29 | required public init(from decoder: Decoder) throws { 30 | let container = try decoder.container(keyedBy: ODCodingKeys.self) 31 | if let interactiveOnboardingPages = try? container.decode([InteractiveOnboardingPage].self, forKey: .pages) { 32 | self.pages = interactiveOnboardingPages 33 | } else { 34 | throw NAError.dataFormat(type: .invalidJSONPayload) 35 | } 36 | if let payload = try? container.decodeIfPresent(String.self, forKey: .progressBarPayload) { 37 | self.progressBarPayload = payload 38 | } 39 | if let outputFile = try? container.decode(String.self, forKey: .outputFile) { 40 | self.outputFile = outputFile 41 | } 42 | } 43 | 44 | public func encode(to encoder: Encoder) throws { 45 | var container = encoder.container(keyedBy: ODCodingKeys.self) 46 | 47 | try container.encodeIfPresent(progressBarPayload, forKey: .progressBarPayload) 48 | try container.encode(outputFile, forKey: .outputFile) 49 | try container.encode(pages, forKey: .pages) 50 | } 51 | 52 | // MARK: Codable protocol conformity - END 53 | } 54 | 55 | /// This object describe an interactive onboarding page. 56 | final class InteractiveOnboardingPage: Codable { 57 | 58 | // MARK: - Variables 59 | 60 | /// The title of the page. 61 | var title: String? 62 | /// The subtitle of the page. 63 | var subtitle: String? 64 | /// The body of the page. 65 | var body: String? 66 | /// The info section showed with the click on the info button. 67 | var infoSection: InfoSection? 68 | /// The path for a custom icon on top of the page 69 | var topIcon: String? 70 | /// Boolean value that define if the page could be browsed multiple time from the user to eventually modify acessorry view's choices. 71 | var singleChange: Bool? 72 | /// The list of accessory views for the page. 73 | var accessoryViews: [[NotificationAccessoryElement]]? 74 | /// A tertiary button available on the page. 75 | var tertiaryButton: NotificationButton? 76 | /// Custom label for the primary button. 77 | var primaryButtonLabel: String? 78 | /// Custom label for the secondary button. 79 | var secondaryButtonLabel: String? 80 | 81 | public func isValidPage() -> Bool { 82 | return title != nil || subtitle != nil || body != nil || (accessoryViews != nil && !(accessoryViews?.isEmpty ?? true)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Shared/Model/UIObjects/ProgressState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressState.swift 3 | // Notification Agent 4 | // 5 | // Created by Simone Martorelli on 10/15/20. 6 | // © Copyright IBM Corp. 2021, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This object describe a state for the progress bar view. 13 | struct ProgressState: Equatable { 14 | /// The percentage of completition of the bar. 15 | var percent: Double 16 | /// The message on top of the bar. 17 | var topMessage: String 18 | /// The message on the bottom of the bar. 19 | var bottomMessage: String 20 | /// If the bar is an indeterminate loading. 21 | var isIndeterminate: Bool = false 22 | /// If the popop should allow user interaction during progressbar loading. 23 | var isUserInteractionEnabled = false 24 | /// If the popop should allow user interruption during progressbar loading. 25 | var isUserInterruptionAllowed = false 26 | /// If the popup should automatically close at the end of the progress. 27 | var exitOnCompletion: Bool = false 28 | 29 | init(_ payload: String? = nil, currentState: ProgressState? = nil) { 30 | self.percent = currentState?.percent ?? 0 31 | self.topMessage = currentState?.topMessage ?? "" 32 | self.bottomMessage = currentState?.bottomMessage ?? "" 33 | self.isIndeterminate = currentState?.isIndeterminate ?? false 34 | self.isUserInteractionEnabled = currentState?.isUserInteractionEnabled ?? false 35 | self.isUserInterruptionAllowed = currentState?.isUserInterruptionAllowed ?? false 36 | self.exitOnCompletion = currentState?.exitOnCompletion ?? false 37 | guard let payload = payload else { return } 38 | guard payload.lowercased() != "end" else { 39 | self.percent = 100 40 | return 41 | } 42 | var splittedStrings = payload.split(separator: "/") 43 | splittedStrings.reverse() 44 | for index in 0..