├── .github
└── workflows
│ ├── build_pkg_beta.yml
│ └── build_pkg_release_manual.yml
├── .gitignore
├── Configuration Profile Samples
├── Background Item Management
│ └── Background Item Management - Root3.mobileconfig
├── PPPC
│ └── PPPC - Support App.mobileconfig
└── Support App Configuration Sample.mobileconfig
├── Extension Sample Scripts
├── Jamf Connect Elevated Privileges
│ └── jamf_connect_elevated_privileges_change.zsh
├── Jamf Last Check-In
│ ├── install_extension-jamf_last_check-in.zsh
│ ├── jamf_check-in.zsh
│ └── jamf_last_check-in_time.zsh
├── User Permissions
│ ├── install_extension-user_permissions.zsh
│ └── user_permissions.zsh
├── battery_condition.zsh
├── mSCP Compliance Status
│ ├── install_extension-mscp_compliance_status.zsh
│ ├── mscp_compliance_status.sh
│ └── mscp_run_remediation.zsh
└── sap_privileges_change_permissions.zsh
├── Jamf Pro Custom Schema
└── Jamf Pro Custom Schema.json
├── LICENSE
├── LaunchAgent Sample
└── nl.root3.support.plist
├── README.md
├── Screenshots
├── app_catalog_integration.png
├── configurable_buttons.png
├── configurable_buttons_2.4.png
├── custom_alert.png
├── example_dark_mode.png
├── example_dark_mode_accentcolor.png
├── example_light_mode.png
├── generic_light_mode.png
├── generic_light_mode_cropped.png
├── generic_version_2.1.png
├── generic_version_2.1_small.png
├── generic_version_2.2.png
├── generic_version_2.2_small.png
├── generic_version_2.2_small_dark.png
├── generic_version_2.3.png
├── generic_version_2.3_small_dark.png
├── generic_version_2.4.png
├── generic_version_2.4_beta.gif
├── generic_version_2.5.png
├── generic_version_2.5_dark.png
├── generic_version_2.6.png
├── how_to_use_sf_symbols.png
├── jamf_pro_custom_schema.png
├── last_reboot.png
├── root3_dark_mode.png
├── root3_light_mode.png
├── root3_light_mode_cropped.png
├── software_update_integration.png
└── welcome_screen.png
├── SupportHelper
├── SupportHelper.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── SupportHelper
│ ├── Info.plist
│ ├── SupportHelper.entitlements
│ └── main.swift
└── pkgbuild
│ ├── SupportHelper-component.plist
│ ├── build_pkg.zsh
│ ├── distribution.xml
│ ├── requirements.plist
│ └── scripts
│ └── postinstall
├── build_pkg_automated.zsh
├── pkgbuild
├── Support-component.plist
├── build_pkg.zsh
├── distribution.xml
├── exportOptions.plist
├── payload
│ └── .DS_Store
├── requirements.plist
└── scripts
│ └── postinstall
└── src
├── Support.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ └── Support.xcscheme
├── Support
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Support-1024.png
│ │ ├── Support-128.png
│ │ ├── Support-16.png
│ │ ├── Support-256.png
│ │ ├── Support-32.png
│ │ ├── Support-512.png
│ │ └── Support-64.png
│ ├── Contents.json
│ ├── DefaultLogo.imageset
│ │ ├── Contents.json
│ │ ├── Support-128.png
│ │ └── Support-256.png
│ └── hoverColor.colorset
│ │ └── Contents.json
├── Base.lproj
│ └── Main.storyboard
├── ComputerInfo.swift
├── Controllers
│ ├── AppCatalogController.swift
│ └── InstallTaskQueue.swift
├── EventMonitor.swift
├── Extensions
│ ├── Extensions.swift
│ └── RemoveEscapingCharacters.swift
├── FileUtilities.swift
├── Info.plist
├── Models
│ ├── KerberosSSOExtensionModel.swift
│ ├── SoftwareUpdateDeclarationModel.swift
│ └── SoftwareUpdateModel.swift
├── NotificationNames.swift
├── Preferences.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── PrivilegedHelper
│ ├── ExecutionService.swift
│ └── HelperRemote.swift
├── Scripts
│ ├── install_privileged_helper_tool.zsh
│ └── uninstall_privileged_helper_tool.zsh
├── Support.entitlements
├── UserInfo.swift
├── Views
│ ├── AppCatalog
│ │ ├── AppUpdatesView.swift
│ │ └── InstalledAppItem.swift
│ ├── AppView.swift
│ ├── ButtonTemplateViews
│ │ ├── InfoItem.swift
│ │ ├── Item.swift
│ │ ├── ItemDouble.swift
│ │ ├── ItemSmall.swift
│ │ └── ProgressBarItem.swift
│ ├── ButtonViews
│ │ ├── AppCatalogSubview.swift
│ │ ├── ComputerNameSubview.swift
│ │ ├── ExtensionASubview.swift
│ │ ├── ExtensionBSubview.swift
│ │ ├── MacOSVersionSubview.swift
│ │ ├── ModelSubView.swift
│ │ ├── NetworkSubview.swift
│ │ ├── PasswordSubview.swift
│ │ ├── StorageSubview.swift
│ │ └── UptimeSubview.swift
│ ├── ContentView.swift
│ ├── EffectsView.swift
│ ├── Experimental
│ │ └── ChangePassword.swift
│ ├── HeaderView.swift
│ ├── LogoView.swift
│ ├── NotificationBadgeTextView.swift
│ ├── NotificationBadgeView.swift
│ ├── PopoverAlertView.swift
│ ├── QuitButton.swift
│ ├── StatusItemBadgeView.swift
│ ├── UpdateView.swift
│ ├── UptimeAlertView.swift
│ ├── ViewModifiers
│ │ └── DarkModeBorder.swift
│ └── WelcomeScreen
│ │ ├── FeatureView.swift
│ │ └── WelcomeScreenView.swift
├── de.lproj
│ └── Localizable.strings
├── en.lproj
│ └── Localizable.strings
├── es.lproj
│ └── Localizable.strings
├── fr.lproj
│ └── Localizable.strings
├── nl-NL.lproj
│ └── Main.strings
├── nl.lproj
│ └── Localizable.strings
└── nl.root3.support.plist
├── SupportHelper
├── AuditTokenHack.h
├── AuditTokenHack.m
├── ConnectionIdentityService.swift
├── HelperConstants.swift
├── HelperExecutionService.swift
├── Info.plist
├── OSStatus+Extensions.swift
├── PrivilegedHelperError.swift
├── RemoteApplicationProtocol.swift
├── SupportHelper.swift
├── SupportHelperProtocol.swift
├── launchd.plist
├── main.swift
└── nl.root3.support.helper-Bridging-Header.h
└── SupportXPC
├── ExecutionService.swift
├── Info.plist
├── SupportXPC.entitlements
├── SupportXPC.swift
├── SupportXPCProtocol.swift
└── main.swift
/.github/workflows/build_pkg_beta.yml:
--------------------------------------------------------------------------------
1 | name: Build and Notarize Support App Beta
2 |
3 | on:
4 | push:
5 | branches:
6 | - development
7 | paths-ignore:
8 | - 'Configuration Profile Samples/**'
9 | - 'Extension Sample Scripts/**'
10 | - 'Jamf Pro Custom Schema/**'
11 | - 'LaunchAgent Sample/**'
12 | - 'Screenshots/**'
13 | - 'SupportHelper/**'
14 | - 'README.md'
15 |
16 | jobs:
17 | build_with_signing:
18 | runs-on: macos-15
19 | environment: production
20 |
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v4
24 |
25 | - name: Bump Build Number and set version number to env
26 | run: |
27 | cd ./src
28 | xcrun agvtool next-version -all
29 |
30 | APP_BUILD_NUMBER=$(xcrun agvtool vers -terse)
31 | echo "Build number: ${APP_BUILD_NUMBER}"
32 | echo "APP_BUILD_NUMBER=${APP_BUILD_NUMBER}" >> $GITHUB_ENV
33 |
34 | APP_VERSION=$(/usr/libexec/PlistBuddy -c Print:CFBundleShortVersionString Support/Info.plist)
35 | echo "Version number: ${APP_VERSION}"
36 | echo "APP_VERSION=${APP_VERSION}" >> $GITHUB_ENV
37 |
38 | - name: Commit Changes
39 | env:
40 | APP_BUILD_NUMBER: ${{ env.APP_BUILD_NUMBER }}
41 | run: |
42 | git add .
43 | git config --local user.email "action@github.com"
44 | git config --local user.name "GitHub Action"
45 | git commit -m "Bump build number to ${APP_BUILD_NUMBER}"
46 |
47 | - name: Push Changes
48 | uses: ad-m/github-push-action@v0.8.0
49 | with:
50 | github_token: ${{ secrets.GITHUB_TOKEN }}
51 | branch: ${{ github.ref }}
52 |
53 | - name: Install Developer ID Application certificate
54 | uses: apple-actions/import-codesign-certs@v2
55 | with:
56 | keychain-password: ${{ github.run_id }}
57 | p12-file-base64: ${{ secrets.DEVELOPER_ID_APPLICATION_BASE64 }}
58 | p12-password: ${{ secrets.DEVELOPER_ID_APPLICATION_PASSWORD }}
59 |
60 | - name: Install Developer ID Installer certificate
61 | uses: apple-actions/import-codesign-certs@v2
62 | with:
63 | create-keychain: false
64 | keychain-password: ${{ github.run_id }}
65 | p12-file-base64: ${{ secrets.DEVELOPER_ID_INSTALLER_BASE64 }}
66 | p12-password: ${{ secrets.DEVELOPER_ID_INSTALLER_PASSWORD }}
67 |
68 | - name: Enable beta watermark
69 | run: |
70 | sed -i '' "s/let betaRelease: Bool = false/let betaRelease: Bool = true/g" ./src/Support/Preferences.swift
71 |
72 | - name: Build macOS app
73 | run: |
74 | ARCHIVE_PATH="./build/Support.xcarchive"
75 | APP_PATH="./build"
76 |
77 | ls -la /Applications
78 |
79 | # Set Xcode version to latest version available
80 | # XCODE_VERSION=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -n 1)
81 | # echo "Path to latest Xcode version: ${XCODE_VERSION}"
82 | XCODE_VERSION="${{vars.XCODE_VERSION}}"
83 |
84 | # Select Xcode version
85 | sudo xcode-select -s "${XCODE_VERSION}"
86 |
87 | # Build and archive app
88 | "${XCODE_VERSION}/Contents/Developer/usr/bin/xcodebuild" clean build -project ./src/Support.xcodeproj -scheme "Support" -configuration Release CODE_SIGN_IDENTITY="Developer ID Application: Root3 B.V. (98LJ4XBGYK)" -archivePath $ARCHIVE_PATH archive
89 | "${XCODE_VERSION}/Contents/Developer/usr/bin/xcodebuild" -archivePath $ARCHIVE_PATH -exportArchive -exportPath $APP_PATH -exportOptionsPlist ./pkgbuild/exportOptions.plist
90 | chmod +x "${APP_PATH}/Support.app"
91 |
92 | - name: Notarize and package macOS app
93 | run:
94 | ./build_pkg_automated.zsh "${{env.APP_VERSION}}" "${{ secrets.APPLE_ID }}" "${{ secrets.APPLE_ID_APP_SPECIFIC_PASSWORD }}" "${{vars.XCODE_VERSION}}"
95 |
96 | - name: Upload package
97 | uses: actions/upload-artifact@v4
98 | with:
99 | name: Support ${{env.APP_VERSION}} Beta (${{ env.APP_BUILD_NUMBER }})
100 | path: build/
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/.github/workflows/build_pkg_release_manual.yml:
--------------------------------------------------------------------------------
1 | name: Build and Notarize Support App - Manual Release
2 |
3 | on: [workflow_dispatch]
4 |
5 | jobs:
6 | build_with_signing:
7 | runs-on: macos-15
8 | environment: production
9 |
10 | steps:
11 | - name: Checkout repository
12 | uses: actions/checkout@v4
13 |
14 | - name: Set build and version number to env
15 | run: |
16 | cd ./src
17 |
18 | APP_BUILD_NUMBER=$(xcrun agvtool vers -terse)
19 | echo "Build number: ${APP_BUILD_NUMBER}"
20 | echo "APP_BUILD_NUMBER=${APP_BUILD_NUMBER}" >> $GITHUB_ENV
21 |
22 | APP_VERSION=$(/usr/libexec/PlistBuddy -c Print:CFBundleShortVersionString Support/Info.plist)
23 | echo "Version number: ${APP_VERSION}"
24 | echo "APP_VERSION=${APP_VERSION}" >> $GITHUB_ENV
25 |
26 | - name: Install Developer ID Application certificate
27 | uses: apple-actions/import-codesign-certs@v2
28 | with:
29 | keychain-password: ${{ github.run_id }}
30 | p12-file-base64: ${{ secrets.DEVELOPER_ID_APPLICATION_BASE64 }}
31 | p12-password: ${{ secrets.DEVELOPER_ID_APPLICATION_PASSWORD }}
32 |
33 | - name: Install Developer ID Installer certificate
34 | uses: apple-actions/import-codesign-certs@v2
35 | with:
36 | create-keychain: false
37 | keychain-password: ${{ github.run_id }}
38 | p12-file-base64: ${{ secrets.DEVELOPER_ID_INSTALLER_BASE64 }}
39 | p12-password: ${{ secrets.DEVELOPER_ID_INSTALLER_PASSWORD }}
40 |
41 | - name: Build macOS app
42 | run: |
43 | ARCHIVE_PATH="./build/Support.xcarchive"
44 | APP_PATH="./build"
45 |
46 | ls -la /Applications
47 |
48 | # Set Xcode version to latest version available
49 | # XCODE_VERSION=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -n 1)
50 | # echo "Path to latest Xcode version: ${XCODE_VERSION}"
51 | XCODE_VERSION="${{vars.XCODE_VERSION}}"
52 |
53 | # Select Xcode version
54 | sudo xcode-select -s "${XCODE_VERSION}"
55 |
56 | # Build and archive app
57 | "${XCODE_VERSION}/Contents/Developer/usr/bin/xcodebuild" clean build -project ./src/Support.xcodeproj -scheme "Support" -configuration Release CODE_SIGN_IDENTITY="Developer ID Application: Root3 B.V. (98LJ4XBGYK)" -archivePath $ARCHIVE_PATH archive
58 | "${XCODE_VERSION}/Contents/Developer/usr/bin/xcodebuild" -archivePath $ARCHIVE_PATH -exportArchive -exportPath $APP_PATH -exportOptionsPlist ./pkgbuild/exportOptions.plist
59 | chmod +x "${APP_PATH}/Support.app"
60 |
61 | - name: Notarize and package macOS app
62 | run:
63 | ./build_pkg_automated.zsh "${{env.APP_VERSION}}" "${{ secrets.APPLE_ID }}" "${{ secrets.APPLE_ID_APP_SPECIFIC_PASSWORD }}" "${{vars.XCODE_VERSION}}"
64 |
65 | - name: Upload package
66 | uses: actions/upload-artifact@v4
67 | with:
68 | name: Support ${{env.APP_VERSION}}
69 | path: build/
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 |
4 | ## User settings
5 | **/xcuserdata/
6 |
--------------------------------------------------------------------------------
/Configuration Profile Samples/Background Item Management/Background Item Management - Root3.mobileconfig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PayloadContent
6 |
7 |
8 | PayloadDescription
9 |
10 | PayloadDisplayName
11 | Background Item Management - Root3
12 | PayloadIdentifier
13 | com.apple.servicemanagement.8AE9DFC3-EC42-4A57-8D75-973B310C81D8
14 | PayloadOrganization
15 | Root3
16 | PayloadType
17 | com.apple.servicemanagement
18 | PayloadUUID
19 | 8AE9DFC3-EC42-4A57-8D75-973B310C81D8
20 | Rules
21 |
22 |
23 | Comment
24 | Root3
25 | RuleType
26 | TeamIdentifier
27 | RuleValue
28 | 98LJ4XBGYK
29 |
30 |
31 |
32 |
33 | PayloadDisplayName
34 | Background Item Management - Root3
35 | PayloadIdentifier
36 | 4D6C18CA-467B-440C-9844-A8FAF2042F97
37 | PayloadScope
38 | System
39 | PayloadType
40 | Configuration
41 | PayloadUUID
42 | 0FB7270B-40C1-4557-9D0A-B3A498AF10AF
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Configuration Profile Samples/PPPC/PPPC - Support App.mobileconfig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PayloadContent
6 |
7 |
8 | PayloadDescription
9 |
10 | PayloadDisplayName
11 | Privacy Preferences Policy Control
12 | PayloadEnabled
13 |
14 | PayloadIdentifier
15 | com.apple.TCC.configuration-profile-policy.C242FB4C-60DB-490B-B517-82B5E0778100
16 | PayloadOrganization
17 | Root3
18 | PayloadType
19 | com.apple.TCC.configuration-profile-policy
20 | PayloadUUID
21 | C242FB4C-60DB-490B-B517-82B5E0778100
22 | PayloadVersion
23 | 1
24 | Services
25 |
26 | SystemPolicyAllFiles
27 |
28 |
29 | Allowed
30 |
31 | CodeRequirement
32 | anchor apple generic and identifier "nl.root3.support" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "98LJ4XBGYK")
33 | Identifier
34 | nl.root3.support
35 | IdentifierType
36 | bundleID
37 | StaticCode
38 |
39 |
40 |
41 | Allowed
42 |
43 | CodeRequirement
44 | anchor apple generic and identifier "nl.root3.support.helper" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "98LJ4XBGYK")
45 | Identifier
46 | /Library/PrivilegedHelperTools/nl.root3.support.helper
47 | IdentifierType
48 | path
49 | StaticCode
50 |
51 |
52 |
53 |
54 |
55 |
56 | PayloadDescription
57 |
58 | PayloadDisplayName
59 | PPPC - Support App
60 | PayloadEnabled
61 |
62 | PayloadIdentifier
63 | 2096DADD-D9C4-4B78-BE43-7785ECD25E9F
64 | PayloadOrganization
65 | Root3
66 | PayloadRemovalDisallowed
67 |
68 | PayloadScope
69 | System
70 | PayloadType
71 | Configuration
72 | PayloadUUID
73 | C02A15F0-9992-4DAD-9783-B32A114D9DFF
74 | PayloadVersion
75 | 1
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Configuration Profile Samples/Support App Configuration Sample.mobileconfig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PayloadContent
6 |
7 |
8 | ExtensionLinkA
9 | defaults write /Library/Preferences/nl.root3.support.plist ExtensionLoadingA -bool true; /usr/local/bin/jamf policy; /usr/local/bin/jamf_last_check-in_time.zsh
10 | ExtensionSymbolA
11 | clock.badge.checkmark.fill
12 | ExtensionTitleA
13 | Last MDM Check-In
14 | ExtensionTypeA
15 | DistributedNotification
16 | FirstRowLinkLeft
17 | com.teamviewer.TeamViewer
18 | FirstRowLinkMiddle
19 | https://support.root3.nl
20 | FirstRowLinkRight
21 | com.jamfsoftware.selfservice.mac
22 | FirstRowSubtitleLeft
23 | Share your screen
24 | FirstRowSubtitleMiddle
25 | Now
26 | FirstRowSubtitleRight
27 | Download Apps
28 | FirstRowSymbolLeft
29 | cursorarrow.and.square.on.square.dashed
30 | FirstRowSymbolMiddle
31 | lifepreserver
32 | FirstRowSymbolRight
33 | cart.badge.plus
34 | FirstRowTitleLeft
35 | Remote Support
36 | FirstRowTitleMiddle
37 | Get Support
38 | FirstRowTitleRight
39 | Self Service
40 | FirstRowTypeLeft
41 | App
42 | FirstRowTypeMiddle
43 | URL
44 | FirstRowTypeRight
45 | App
46 | FooterText
47 | Provided by your **IT department** with ❤️
48 | HideFirstRow
49 |
50 | HideSecondRow
51 |
52 | InfoItemFive
53 | Password
54 | InfoItemSix
55 | ExtensionA
56 | Logo
57 | /Library/Application Support/JAMF/swift-64x64_2x.png
58 | NotificationIcon
59 | /Library/Application Support/JAMF/swift-64x64_2x.png
60 | OnAppearAction
61 | /usr/local/bin/jamf_last_check-in_time.zsh
62 | PasswordExpiryLimit
63 | 14
64 | PasswordLabel
65 | Cloud Password
66 | PasswordType
67 | KerberosSSO
68 | ShowWelcomeScreen
69 |
70 | StatusBarIconNotifierEnabled
71 |
72 | StorageLimit
73 | 90
74 | Title
75 | Hello **Support App** 2.4
76 | UptimeDaysLimit
77 | 21
78 | PayloadDescription
79 |
80 | PayloadDisplayName
81 | Custom
82 | PayloadEnabled
83 |
84 | PayloadIdentifier
85 | nl.root3.support.E07B484A-FC4A-450B-A0E9-3BC0B737974B
86 | PayloadOrganization
87 | Root3
88 | PayloadType
89 | nl.root3.support
90 | PayloadUUID
91 | E07B484A-FC4A-450B-A0E9-3BC0B737974B
92 | PayloadVersion
93 | 1
94 |
95 |
96 | PayloadDescription
97 |
98 | PayloadDisplayName
99 | Support App Configuration
100 | PayloadEnabled
101 |
102 | PayloadIdentifier
103 | BDA1AE71-4F70-4D93-9924-F8E77E8F0F10
104 | PayloadOrganization
105 | Root3
106 | PayloadRemovalDisallowed
107 |
108 | PayloadScope
109 | System
110 | PayloadType
111 | Configuration
112 | PayloadUUID
113 | 164671D3-3656-41FF-A387-E3229001BB9B
114 | PayloadVersion
115 | 1
116 |
117 |
118 |
--------------------------------------------------------------------------------
/Extension Sample Scripts/Jamf Connect Elevated Privileges/jamf_connect_elevated_privileges_change.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Support App Extension - Jamf Connect Elevated Privileges Change
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # Support App Extension to change the Jamf Connect Elevated Privileges Status
9 | #
10 | # REQUIREMENTS:
11 | # - Jamf Connect
12 | #
13 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
18 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
20 | # --------------------- do not edit below this line ----------------------
21 |
22 | # Support App preference plist
23 | preference_file_location="/Library/Preferences/nl.root3.support.plist"
24 |
25 | # Start spinning indicator
26 | defaults write "${preference_file_location}" ExtensionLoadingB -bool true
27 |
28 | # Replace value with placeholder while loading
29 | defaults write "${preference_file_location}" ExtensionValueB -string "KeyPlaceholder"
30 |
31 | # Get the username of the currently logged in user
32 | username=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }')
33 |
34 | # Check if user is administrator
35 | is_admin=$(dsmemberutil checkmembership -U "${username}" -G admin)
36 |
37 | # Change permissions
38 | if [[ ${is_admin} != *not* ]]; then
39 | sudo -u ${username} /usr/local/bin/jamfconnect acc-promo --demote
40 | else
41 | sudo -u ${username} /usr/local/bin/jamfconnect acc-promo --elevate
42 | fi
43 |
44 | # Run script to populate new values in Extension
45 | /usr/local/bin/user_permissions.zsh
46 |
--------------------------------------------------------------------------------
/Extension Sample Scripts/Jamf Last Check-In/jamf_check-in.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Support App Extension - Jamf Pro Last Check-In Time
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # Support App Extension to get the Jamf Pro Last Check-In Time
9 | #
10 | # REQUIREMENTS:
11 | # - Jamf Pro Binary
12 | #
13 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
18 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
20 | # ------------------ edit the variables below this line ------------------
21 |
22 | # Enable 24 hour clock format. 12 hour clock enabled by default
23 | twenty_four_hour_format="true"
24 |
25 | # --------------------- do not edit below this line ----------------------
26 |
27 | # Support App preference plist
28 | preference_file_location="/Library/Preferences/nl.root3.support.plist"
29 |
30 | # Start spinning indicator
31 | defaults write "${preference_file_location}" ExtensionLoadingA -bool true
32 |
33 | # Replace value with placeholder while loading
34 | defaults write "${preference_file_location}" ExtensionValueA -string "Checking in..."
35 |
36 | # Perform a Jamf Pro check-in
37 | /usr/local/bin/jamf policy
38 |
39 | # Run script to populate new values in Extension
40 | /usr/local/bin/jamf_last_check-in_time.zsh
--------------------------------------------------------------------------------
/Extension Sample Scripts/Jamf Last Check-In/jamf_last_check-in_time.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Support App Extension - Jamf Pro Last Check-In Time
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # Support App Extension to get the Jamf Pro Last Check-In Time
9 | #
10 | # REQUIREMENTS:
11 | # - Jamf Pro Binary
12 | #
13 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
18 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
20 | # ------------------ edit the variables below this line ------------------
21 |
22 | # Enable 24 hour clock format. 12 hour clock enabled by default
23 | twenty_four_hour_format="true"
24 |
25 | # --------------------- do not edit below this line ----------------------
26 |
27 | # Support App preference plist
28 | preference_file_location="/Library/Preferences/nl.root3.support.plist"
29 |
30 | # Start spinning indicator
31 | defaults write "${preference_file_location}" ExtensionLoadingA -bool true
32 |
33 | # Replace value with placeholder while loading
34 | defaults write "${preference_file_location}" ExtensionValueA -string "KeyPlaceholder"
35 |
36 | # Keep loading effect active for 0.5 seconds
37 | sleep 0.5
38 |
39 | # Get last Jamf Pro check-in time from jamf.log
40 | last_check_in_time=$(grep "Checking for policies triggered by \"recurring check-in\"" "/private/var/log/jamf.log" | tail -n 1 | awk '{ print $2,$3,$4 }')
41 |
42 | # Convert last Jamf Pro check-in time to epoch
43 | last_check_in_time_epoch=$(date -j -f "%b %d %T" "${last_check_in_time}" +"%s")
44 |
45 | # Convert last Jamf Pro epoch to something easier to read
46 | if [[ "${twenty_four_hour_format}" == "true" ]]; then
47 | # Outputs 24 hour clock format
48 | last_check_in_time_human_reable=$(date -r "${last_check_in_time_epoch}" "+%A %H:%M")
49 | else
50 | # Outputs 12 hour clock format
51 | last_check_in_time_human_reable=$(date -r "${last_check_in_time_epoch}" "+%A %I:%M %p")
52 | fi
53 |
54 | # Write output to Support App preference plist
55 | defaults write "${preference_file_location}" ExtensionValueA -string "${last_check_in_time_human_reable}"
56 |
57 | # Stop spinning indicator
58 | defaults write "${preference_file_location}" ExtensionLoadingA -bool false
59 |
--------------------------------------------------------------------------------
/Extension Sample Scripts/User Permissions/install_extension-user_permissions.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Install Support App Extension - User Permissions
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # This script will install the Support App Extension script to get the current
9 | # user permission schema. Use this script with your management solution to
10 | # install it locally on the Mac and set the proper permissions.
11 | #
12 | # REQUIREMENTS: -
13 | #
14 | # EXAMPLE:
15 | # Here's an example how to configure the Support App preferences for Extension A
16 | # - ExtensionTitleA: Account Privileges
17 | # - ExtensionSymbolA: wallet.pass.fill
18 | # - ExtensionTypeA: PrivilegedScript
19 | # - ExtensionLinkA: /usr/local/bin/user_permissions.zsh
20 | # - OnAppearAction: /usr/local/bin/user_permissions.zsh
21 | #
22 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
25 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
26 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
27 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 |
29 | # ------------------ edit the variables below this line ------------------
30 |
31 | # Local script path to create
32 | path_to_script="/usr/local/bin/user_permissions.zsh"
33 |
34 | # Set Support App Extension A or B
35 | extension_label="B"
36 |
37 | # --------------------- do not edit below this line ----------------------
38 |
39 | # Script directory
40 | script_directory=$(dirname "${path_to_script}")
41 |
42 | # Create directory if it doesn't exist yet
43 | if [[ ! -d "${script_directory}" ]]; then
44 | mkdir -p "${script_directory}"
45 | fi
46 |
47 | # Write local script
48 | cat > "${path_to_script}" </dev/null)
48 | if [[ "$EXEMPTIONS" == "true" ]]; then
49 | EXEMPT_RULES+=($rule)
50 | fi
51 | done
52 |
53 | unset $rules
54 |
55 | # Get the Findings
56 | auditfile="/Library/Preferences/${audit}"
57 | rules=($(/usr/libexec/PlistBuddy -c "print :" "${auditfile}" | /usr/bin/awk '/Dict/ { print $1 }'))
58 |
59 | for rule in ${rules[*]}; do
60 | if [[ $rule == "Dict" ]]; then
61 | continue
62 | fi
63 | FINDING=$(/usr/libexec/PlistBuddy -c "print :$rule:finding" "${auditfile}")
64 | if [[ "$FINDING" == "true" ]]; then
65 | FAILED_RULES+=($rule)
66 | fi
67 | done
68 | # count items only in Findings
69 | count=0
70 | for finding in ${FAILED_RULES[@]}; do
71 | if [[ ! " ${EXEMPT_RULES[*]} " =~ " ${finding} " ]] ;then
72 | ((count=count+1))
73 | fi
74 | done
75 | else
76 | count="-2"
77 | fi
78 | else
79 | count="-1"
80 | fi
81 |
82 | #### Support App integration ####
83 |
84 | # Start spinning indicator
85 | defaults write /Library/Preferences/nl.root3.support.plist ExtensionLoadingA -bool true
86 |
87 | # Show placeholder value while loading
88 | defaults write /Library/Preferences/nl.root3.support.plist ExtensionValueA -string "KeyPlaceholder"
89 |
90 | # Keep loading effect active for 0.5 seconds
91 | sleep 0.5
92 |
93 | # Set compliance status. If there are 1 or more issues, show the issue count and trigger warning in menu bar icon and info item
94 | if [[ ${count} -gt 0 ]]; then
95 | defaults write "/Library/Preferences/nl.root3.support.plist" ExtensionValueA "Your \$LocalModelShortName has ${count} issues"
96 | defaults write "/Library/Preferences/nl.root3.support.plist" ExtensionAlertA -bool true
97 | else
98 | defaults write "/Library/Preferences/nl.root3.support.plist" ExtensionValueA "Your \$LocalModelShortName is secure"
99 | defaults write "/Library/Preferences/nl.root3.support.plist" ExtensionAlertA -bool false
100 | fi
101 |
102 | # Stop loading effect
103 | defaults write "/Library/Preferences/nl.root3.support.plist" ExtensionLoadingA -bool false
104 |
--------------------------------------------------------------------------------
/Extension Sample Scripts/mSCP Compliance Status/mscp_run_remediation.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Support App Extension - macOS Security Compliance Project Remediation
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # Support App Extension to run macOS Security Compliance Project Remediation.
9 | #
10 | # REQUIREMENTS:
11 | # - Jamf Pro Binary
12 | #
13 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
18 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
20 | # ------------------ edit the variables below this line ------------------
21 |
22 | # Policy custom trigger
23 | custom_trigger="mscp_remediation"
24 |
25 | # --------------------- do not edit below this line ----------------------
26 |
27 | # Support App preference plist
28 | preference_file_location="/Library/Preferences/nl.root3.support.plist"
29 |
30 | # Start spinning indicator
31 | defaults write "${preference_file_location}" ExtensionLoadingB -bool true
32 |
33 | # Replace value with placeholder while loading
34 | defaults write "${preference_file_location}" ExtensionValueB -string "Remediating..."
35 |
36 | # Call the Jamf Pro custom trigger with remediation policy
37 | /usr/local/bin/jamf policy -event ${custom_trigger}
38 |
39 | # Run script to populate new values in Extension
40 | /usr/local/bin/mscp_compliance_status.sh
--------------------------------------------------------------------------------
/Extension Sample Scripts/sap_privileges_change_permissions.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Support App Extension - SAP Privileges Change Permissions
4 | #
5 | #
6 | # Copyright 2022 Root3 B.V. All rights reserved.
7 | #
8 | # Support App Extension to change user permissions with SAP Privileges.
9 | #
10 | # REQUIREMENTS:
11 | # - Jamf Pro Binary
12 | # - SAP Privileges: https://github.com/SAP/macOS-enterprise-privileges
13 | #
14 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
17 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
19 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | # --------------------- do not edit below this line ----------------------
22 |
23 | # Support App preference plist
24 | preference_file_location="/Library/Preferences/nl.root3.support.plist"
25 |
26 | # SAP Privileges CLI
27 | sap_privileges_cli="/Applications/Privileges.app/Contents/macOS/PrivilegesCLI"
28 |
29 | # Start spinning indicator
30 | defaults write "${preference_file_location}" ExtensionLoadingB -bool true
31 |
32 | # Replace value with placeholder while loading
33 | defaults write "${preference_file_location}" ExtensionValueB -string "KeyPlaceholder"
34 |
35 | # Get the username and uid of the currently logged in user
36 | username=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }')
37 | uid=$(id -u "$username")
38 |
39 | # Check if user is administrator
40 | is_admin=$(dsmemberutil checkmembership -U "${username}" -G admin)
41 |
42 | # Change permissions
43 | if [[ ${is_admin} != *not* ]]; then
44 | launchctl asuser "$uid" sudo -u ${username} ${sap_privileges_cli} --remove
45 | else
46 | launchctl asuser "$uid" sudo -u ${username} ${sap_privileges_cli} --add
47 | fi
48 |
49 | # Run Support App Extension to report new permission status
50 | "/usr/local/bin/user_permissions.zsh"
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Root3
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LaunchAgent Sample/nl.root3.support.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Label
6 | nl.root3.support
7 | Program
8 | /Applications/Support.app/Contents/MacOS/Support
9 | KeepAlive
10 |
11 | ProcessType
12 | Interactive
13 | AssociatedBundleIdentifiers
14 | nl.root3.support
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Screenshots/app_catalog_integration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/app_catalog_integration.png
--------------------------------------------------------------------------------
/Screenshots/configurable_buttons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/configurable_buttons.png
--------------------------------------------------------------------------------
/Screenshots/configurable_buttons_2.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/configurable_buttons_2.4.png
--------------------------------------------------------------------------------
/Screenshots/custom_alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/custom_alert.png
--------------------------------------------------------------------------------
/Screenshots/example_dark_mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/example_dark_mode.png
--------------------------------------------------------------------------------
/Screenshots/example_dark_mode_accentcolor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/example_dark_mode_accentcolor.png
--------------------------------------------------------------------------------
/Screenshots/example_light_mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/example_light_mode.png
--------------------------------------------------------------------------------
/Screenshots/generic_light_mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_light_mode.png
--------------------------------------------------------------------------------
/Screenshots/generic_light_mode_cropped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_light_mode_cropped.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.1.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.1_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.1_small.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.2.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.2_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.2_small.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.2_small_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.2_small_dark.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.3.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.3_small_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.3_small_dark.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.4.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.4_beta.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.4_beta.gif
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.5.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.5_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.5_dark.png
--------------------------------------------------------------------------------
/Screenshots/generic_version_2.6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/generic_version_2.6.png
--------------------------------------------------------------------------------
/Screenshots/how_to_use_sf_symbols.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/how_to_use_sf_symbols.png
--------------------------------------------------------------------------------
/Screenshots/jamf_pro_custom_schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/jamf_pro_custom_schema.png
--------------------------------------------------------------------------------
/Screenshots/last_reboot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/last_reboot.png
--------------------------------------------------------------------------------
/Screenshots/root3_dark_mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/root3_dark_mode.png
--------------------------------------------------------------------------------
/Screenshots/root3_light_mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/root3_light_mode.png
--------------------------------------------------------------------------------
/Screenshots/root3_light_mode_cropped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/root3_light_mode_cropped.png
--------------------------------------------------------------------------------
/Screenshots/software_update_integration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/software_update_integration.png
--------------------------------------------------------------------------------
/Screenshots/welcome_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/Screenshots/welcome_screen.png
--------------------------------------------------------------------------------
/SupportHelper/SupportHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SupportHelper/SupportHelper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SupportHelper/SupportHelper/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIdentifier
6 | $(PRODUCT_BUNDLE_IDENTIFIER)
7 | CFBundleName
8 | $(PRODUCT_NAME)
9 | CFBundleShortVersionString
10 | $(MARKETING_VERSION)
11 |
12 |
13 |
--------------------------------------------------------------------------------
/SupportHelper/SupportHelper/SupportHelper.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/SupportHelper/SupportHelper/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // SupportHelper
4 | //
5 | // Created by Jordy Witteman on 02/11/2021.
6 | //
7 |
8 | import Foundation
9 | import AppKit
10 | import os
11 |
12 | class SupportHelper {
13 |
14 | // Notification name when a button is clicked
15 | let notificationNameAction = "nl.root3.support.Action"
16 |
17 | // Notification name when the Support App popover appears
18 | let notificationNameSupportAppeared = "nl.root3.support.SupportAppeared"
19 |
20 | // Unified Logging
21 | let logger = Logger(subsystem: "nl.root3.support.helper", category: "Helper")
22 |
23 | // Support App UserDefaults
24 | let supportDefaults = UserDefaults(suiteName: "nl.root3.support")
25 |
26 | init() {
27 | DistributedNotificationCenter.default().addObserver(
28 | forName: Notification.Name(notificationNameAction),
29 | object: nil,
30 | queue: nil,
31 | using: self.gotNotification(notification:)
32 | )
33 |
34 | DistributedNotificationCenter.default().addObserver(
35 | forName: Notification.Name(notificationNameSupportAppeared),
36 | object: nil,
37 | queue: nil,
38 | using: self.gotNotificationSupportAppeared(notification:)
39 | )
40 | }
41 |
42 | // Function to run script or command when a button is clicked
43 | func gotNotification(notification: Notification) {
44 | logger.debug("Received Distributed Notification: \(notification, privacy: .public)")
45 |
46 | // Get action from Configuration Profile
47 | let action = supportDefaults?.string(forKey: notification.object as! String) ?? ""
48 | guard !action.isEmpty else {
49 | logger.error("No action defined for key \(notification.object as! String, privacy: .public). Please set this value in the Support App Configuration Profile")
50 | return
51 | }
52 | logger.debug("Action: \(action, privacy: .public)")
53 |
54 | // Check value comes from a Configuration Profile. If not, the command or script may be maliciously set and needs to be ignored
55 | if supportDefaults?.objectIsForced(forKey: notification.object as! String) == true {
56 | let task = Process()
57 | task.launchPath = "/bin/zsh"
58 | task.arguments = ["-c", action]
59 | task.launch()
60 | } else {
61 | logger.error("Action \"\(action, privacy: .public)\" is not set by an administrator and potentially dangerous. Action will not be executed")
62 | }
63 | }
64 |
65 | // Function to run script or command when the Support App popover appears
66 | func gotNotificationSupportAppeared(notification: Notification) {
67 | logger.debug("Received Distributed Notification: \(notification, privacy: .public)")
68 |
69 | // Get action from Configuration Profile
70 | let action = supportDefaults?.string(forKey: "OnAppearAction") ?? ""
71 | guard !action.isEmpty else {
72 | logger.error("No action defined for OnAppearAction. Please set this value in the Support App Configuration Profile")
73 | return
74 | }
75 | logger.debug("Action: \(action, privacy: .public)")
76 |
77 | // Check value comes from a Configuration Profile. If not, the command or script may be maliciously set and needs to be ignored
78 | if supportDefaults?.objectIsForced(forKey: "OnAppearAction") == true {
79 | let task = Process()
80 | task.launchPath = "/bin/zsh"
81 | task.arguments = ["-c", action]
82 | task.launch()
83 | } else {
84 | logger.error("Action \"\(action, privacy: .public)\" is not set by an administrator and potentially dangerous. Action will not be executed")
85 | }
86 | }
87 | }
88 |
89 | let helper = SupportHelper()
90 |
91 | dispatchMain()
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/SupportHelper/pkgbuild/SupportHelper-component.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | BundleHasStrictIdentifier
7 |
8 | BundleIsRelocatable
9 |
10 | BundleIsVersionChecked
11 |
12 | BundleOverwriteAction
13 | upgrade
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/SupportHelper/pkgbuild/build_pkg.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Build SupportHelper Package
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # This script will build the SupportHelper Package
9 | #
10 | # USAGE:
11 | # - Make sure an Keychain profile is stored for notarytool
12 | # - Export SupportHelper binary to pkguild folder
13 | # - Navigate to folder: pkgbuild/payload
14 | # - Run the script: /build_pkg.zsh TARGET_VERSION_HERE
15 | #
16 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
19 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
21 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
23 | # ------------------ edit the variables below this line ------------------
24 |
25 | # Exit on error
26 | set -e
27 |
28 | # App Name
29 | app_name="SupportHelper"
30 |
31 | # App Bundle Identifier
32 | bundle_identifier="nl.root3.support.helper"
33 |
34 | # App Version
35 | version=$1
36 |
37 | # Path to folder with payload
38 | payload="payload"
39 |
40 | # Path to folder with scripts
41 | scripts="scripts"
42 |
43 | # Path to Component plist
44 | component_plist="SupportHelper-component.plist"
45 |
46 | # Requirements plist
47 | requirements_plist="requirements.plist"
48 |
49 | # Distribution xml
50 | distribution_xml="distribution.xml"
51 |
52 | # Install location
53 | install_location="/usr/local/bin"
54 |
55 | # Developer ID Installer certificate from Keychain
56 | signing_identity="Developer ID Installer: Root3 B.V. (98LJ4XBGYK)"
57 |
58 | # Name of the Keychain profile used for notarytool
59 | keychain_profile="Root3"
60 |
61 | # --------------------- do not edit below this line ----------------------
62 |
63 | # Exit when nu version is specified
64 | if [[ -z ${version} ]]; then
65 | echo "No version specified, add version as argument when running this script"
66 | exit 1
67 | fi
68 |
69 | # Get the username of the currently logged in user
70 | username=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }')
71 |
72 | # NFS Home Directory of user
73 | nfs_home_directory=$(dscl . read /Users/${username} NFSHomeDirectory | awk '{print $2}')
74 |
75 | # Create directory
76 | mkdir -p "${nfs_home_directory}/Downloads/${app_name}_${version}"
77 |
78 | # Build and export pkg to Downloads folder
79 | pkgbuild --root "${payload}" \
80 | --scripts "${scripts}" \
81 | --install-location "${install_location}" \
82 | --identifier "${bundle_identifier}" \
83 | --version "${version}" \
84 | "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name}_component.pkg"
85 |
86 | # Create basic Distribution file
87 | # productbuild --synthesize \
88 | # --package "${nfs_home_directory}/Downloads/${app_name}_${version}/Support_component.pkg" \
89 | # --product "${requirements_plist}" \
90 | # "${nfs_home_directory}/Downloads/${app_name}_${version}/distribution.xml"
91 |
92 | # Create distribution package to support InstallApplication MDM command
93 | productbuild --distribution "${distribution_xml}" \
94 | --package-path "${nfs_home_directory}/Downloads/${app_name}_${version}/" \
95 | "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}_dist.pkg"
96 |
97 | # Sign package
98 | productsign --sign "${signing_identity}" \
99 | "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}_dist.pkg" \
100 | "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}.pkg"
101 |
102 | # Submit pkg to notarytool
103 | xcrun notarytool submit "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}.pkg" \
104 | --keychain-profile "${keychain_profile}" \
105 | --wait
106 |
107 | # Staple the notarization ticket to the pkg
108 | xcrun stapler staple "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}.pkg"
109 |
110 | # Check the notarization ticket validity
111 | spctl --assess -vv --type install "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}.pkg"
112 |
--------------------------------------------------------------------------------
/SupportHelper/pkgbuild/distribution.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SupportHelper
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | SupportHelper_component.pkg
21 |
--------------------------------------------------------------------------------
/SupportHelper/pkgbuild/requirements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | os
6 |
7 | 11
8 |
9 |
10 |
--------------------------------------------------------------------------------
/SupportHelper/pkgbuild/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Install SupportHelper LaunchDaemon
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # This script will create the SupportHelper LaunchDaemon and reload it when
9 | # needed.
10 | #
11 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
12 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
13 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
14 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
15 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
16 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 |
18 | # LaunchDaemon label
19 | launch_daemon="nl.root3.support.helper"
20 |
21 | # CLI location
22 | cli_location="/usr/local/bin/SupportHelper"
23 |
24 | # Create the LaunchAgent
25 | defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" Label -string "${launch_daemon}"
26 | defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" ProgramArguments -array -string "${cli_location}"
27 | # Keep the process alive
28 | defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" KeepAlive -boolean yes
29 | # Run at every reboot
30 | defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" RunAtLoad -boolean yes
31 | # Set permissions
32 | chown root:wheel "/Library/LaunchDaemons/${launch_daemon}.plist"
33 | chmod 644 "/Library/LaunchDaemons/${launch_daemon}.plist"
34 |
35 | # Unload the LaunchDaemon
36 | launchctl bootout system "/Library/LaunchDaemons/${launch_daemon}.plist" &> /dev/null
37 | # Load the LaunchAgent
38 | launchctl bootstrap system "/Library/LaunchDaemons/${launch_daemon}.plist"
39 |
--------------------------------------------------------------------------------
/build_pkg_automated.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Build Support App Package
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # This script will build the Support App package
9 | #
10 | # USAGE:
11 | # - GitHub Actions
12 | #
13 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
18 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
20 | # ------------------ edit the variables below this line ------------------
21 |
22 | # Exit on error
23 | set -e
24 |
25 | # Current directory
26 | current_directory=$(dirname $0)
27 |
28 | # App Version
29 | version=$1
30 |
31 | # Apple ID used to notarize the package
32 | apple_id=$2
33 |
34 | # Apple ID password
35 | apple_id_app_specific_password=$3
36 |
37 | # Xcode version
38 | xcode_version=$4
39 |
40 | # App Name
41 | app_name="Support"
42 |
43 | # App Bundle Identifier
44 | bundle_identifier="nl.root3.support"
45 |
46 | # Path to folder with payload
47 | payload="${current_directory}/pkgbuild/payload"
48 |
49 | # Path to folder with scripts
50 | scripts="${current_directory}/pkgbuild/scripts"
51 |
52 | # Path to Component plist
53 | component_plist="${current_directory}/pkgbuild/Support-component.plist"
54 |
55 | # Requirements plist
56 | requirements_plist="${current_directory}/pkgbuild/requirements.plist"
57 |
58 | # Distribution xml
59 | distribution_xml="${current_directory}/pkgbuild/distribution.xml"
60 |
61 | # Install location
62 | install_location="/Applications"
63 |
64 | # Developer ID Installer certificate from Keychain
65 | signing_identity="Developer ID Installer: Root3 B.V. (98LJ4XBGYK)"
66 |
67 | # Name of the Keychain profile used for notarytool
68 | keychain_profile="Root3"
69 |
70 | # --------------------- do not edit below this line ----------------------
71 |
72 | # Create directory
73 | mkdir -p "${current_directory}/${app_name}"
74 |
75 | # Create directory
76 | if [[ ! -d "${payload}" ]]; then
77 | echo "Creating ${payload}"
78 | mkdir "${payload}"
79 | fi
80 |
81 | # Move app bundle to payload folder
82 | cp -r "${current_directory}/build/${app_name}.app" "${payload}"
83 |
84 | # Set credentials for notarization
85 | echo "Xcode version: ${xcode_version}"
86 | "${xcode_version}/Contents/Developer/usr/bin/notarytool" store-credentials --apple-id "${apple_id}" --team-id "98LJ4XBGYK" --password "${apple_id_app_specific_password}" "${keychain_profile}"
87 |
88 | # Build and sign pkg
89 | pkgbuild --component-plist "${component_plist}" \
90 | --root "${payload}" \
91 | --scripts "${scripts}" \
92 | --install-location "${install_location}" \
93 | --identifier "${bundle_identifier}" \
94 | --version "${version}" \
95 | "${current_directory}/${app_name}/${app_name}_component.pkg"
96 |
97 | # Create distribution package to support InstallApplication MDM command
98 | productbuild --distribution "${distribution_xml}" \
99 | --package-path "${current_directory}/${app_name}/" \
100 | "${current_directory}/${app_name}/${app_name} ${version}_dist.pkg"
101 |
102 | # Sign package
103 | productsign --sign "${signing_identity}" \
104 | "${current_directory}/${app_name}/${app_name} ${version}_dist.pkg" \
105 | "${current_directory}/${app_name}/${app_name} ${version}.pkg"
106 |
107 | # Submit pkg to notarytool
108 | "${xcode_version}/Contents/Developer/usr/bin/notarytool" submit "${current_directory}/${app_name}/${app_name} ${version}.pkg" \
109 | --keychain-profile "${keychain_profile}" \
110 | --wait
111 |
112 | # Staple the notarization ticket to the pkg
113 | "${xcode_version}/Contents/Developer/usr/bin/stapler" staple "${current_directory}/${app_name}/${app_name} ${version}.pkg"
114 |
115 | # Check the notarization ticket validity
116 | spctl --assess -vv --type install "${current_directory}/${app_name}/${app_name} ${version}.pkg"
117 |
118 | # Move package to build folder
119 | mv "${current_directory}/${app_name}/${app_name} ${version}.pkg" "${current_directory}/build"
--------------------------------------------------------------------------------
/pkgbuild/Support-component.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | BundleHasStrictIdentifier
7 |
8 | BundleIsRelocatable
9 |
10 | BundleIsVersionChecked
11 |
12 | BundleOverwriteAction
13 | upgrade
14 | RootRelativeBundlePath
15 | Support.app
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/pkgbuild/build_pkg.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Build Support App Package
4 | #
5 | #
6 | # Copyright 2022 Root3 B.V. All rights reserved.
7 | #
8 | # This script will build the Support App Package
9 | #
10 | # USAGE:
11 | # - Make sure an Keychain profile is stored for notarytool
12 | # - Export .app to pkguild folder
13 | # - Navigate to folder: pkgbuild/payload
14 | # - Run the script: /build_pkg.zsh TARGET_VERSION_HERE
15 | #
16 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
19 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
21 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
23 | # ------------------ edit the variables below this line ------------------
24 |
25 | # Exit on error
26 | set -e
27 |
28 | # App Name
29 | app_name="Support"
30 |
31 | # App Bundle Identifier
32 | bundle_identifier="nl.root3.support"
33 |
34 | # App Version
35 | version=$1
36 |
37 | # Path to folder with payload
38 | payload="payload"
39 |
40 | # Path to folder with scripts
41 | scripts="scripts"
42 |
43 | # Path to Component plist
44 | component_plist="Support-component.plist"
45 |
46 | # Requirements plist
47 | requirements_plist="requirements.plist"
48 |
49 | # Distribution xml
50 | distribution_xml="distribution.xml"
51 |
52 | # Install location
53 | install_location="/Applications"
54 |
55 | # Developer ID Installer certificate from Keychain
56 | signing_identity="Developer ID Installer: Root3 B.V. (98LJ4XBGYK)"
57 |
58 | # Name of the Keychain profile used for notarytool
59 | keychain_profile="Root3"
60 |
61 | # --------------------- do not edit below this line ----------------------
62 |
63 | # Exit when nu version is specified
64 | if [[ -z ${version} ]]; then
65 | echo "No version specified, add version as argument when running this script"
66 | exit 1
67 | fi
68 |
69 | # Get the username of the currently logged in user
70 | username=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }')
71 |
72 | # NFS Home Directory of user
73 | nfs_home_directory=$(dscl . read /Users/${username} NFSHomeDirectory | awk '{print $2}')
74 |
75 | # Create directory
76 | mkdir -p "${nfs_home_directory}/Downloads/${app_name}_${version}"
77 |
78 | # Build and export pkg to Downloads folder
79 | pkgbuild --component-plist "${component_plist}" \
80 | --root "${payload}" \
81 | --scripts "${scripts}" \
82 | --install-location "${install_location}" \
83 | --identifier "${bundle_identifier}" \
84 | --version "${version}" \
85 | "${nfs_home_directory}/Downloads/${app_name}_${version}/Support_component.pkg"
86 |
87 | # Create basic Distribution file
88 | # productbuild --synthesize \
89 | # --package "${nfs_home_directory}/Downloads/${app_name}_${version}/Support_component.pkg" \
90 | # --product "${requirements_plist}" \
91 | # "${nfs_home_directory}/Downloads/${app_name}_${version}/distribution.xml"
92 |
93 | # Create distribution package to support InstallApplication MDM command
94 | productbuild --distribution "${distribution_xml}" \
95 | --package-path "${nfs_home_directory}/Downloads/${app_name}_${version}/" \
96 | "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}_dist.pkg"
97 |
98 | # Sign package
99 | productsign --sign "${signing_identity}" \
100 | "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}_dist.pkg" \
101 | "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}.pkg"
102 |
103 | # Submit pkg to notarytool
104 | xcrun notarytool submit "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}.pkg" \
105 | --keychain-profile "${keychain_profile}" \
106 | --wait
107 |
108 | # Staple the notarization ticket to the pkg
109 | xcrun stapler staple "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}.pkg"
110 |
111 | # Check the notarization ticket validity
112 | spctl --assess -vv --type install "${nfs_home_directory}/Downloads/${app_name}_${version}/${app_name} ${version}.pkg"
113 |
--------------------------------------------------------------------------------
/pkgbuild/distribution.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Support App
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Support_component.pkg
21 |
--------------------------------------------------------------------------------
/pkgbuild/exportOptions.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | method
6 | developer-id
7 |
8 |
--------------------------------------------------------------------------------
/pkgbuild/payload/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/pkgbuild/payload/.DS_Store
--------------------------------------------------------------------------------
/pkgbuild/requirements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | os
6 |
7 | 12
8 |
9 |
10 |
--------------------------------------------------------------------------------
/pkgbuild/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Install Support App LaunchAgent
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # This script will create the Support App LaunchAgent and reload it when needed.
9 | #
10 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
11 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
12 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
13 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
14 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
15 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | # ------------------ edit the variables below this line ------------------
18 |
19 | # LaunchAgent label
20 | launch_agent="nl.root3.support"
21 |
22 | # LaunchDaemon label
23 | launch_daemon="nl.root3.support.helper"
24 |
25 | # Install location
26 | install_location="/Applications/Support.app"
27 |
28 | # Get the username of the currently logged in user
29 | username=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }')
30 |
31 | # Remove "Downloaded from Internet" warning
32 | # xattr -d -r com.apple.quarantine "${install_location}"
33 |
34 | # Load Requirements
35 | autoload is-at-least
36 |
37 | # macOS Version
38 | os_version=$(sw_vers -productVersion)
39 |
40 | # ------------------ Gatekeeper scan ------------------
41 |
42 | # Perform a Gatekeeper scan. This is useful for pre-warming the cache so users
43 | # do not see the 'Verifying...' dialog on first launch of an application.
44 | if is-at-least 14.0 ${os_version}; then
45 | gktool scan "${install_location}"
46 | fi
47 |
48 | # ------------------ LaunchAgent ------------------
49 |
50 | # Open the app so the legacy LaunchAgent properly displays the app name and icon
51 | # in System Settings > General > Login Items
52 | # Tested and does only work when a user is logged in during the installation
53 | # Thanks to @PicoMitchell:
54 | # https://github.com/freegeek-pdx/macOS-Testing-and-Deployment-Scripts/blob/main/fgMIB%20Resources/Prepare%20OS%20Package/fg-prepare-os.sh#L971-L983
55 | if is-at-least 13.0 ${os_version}; then
56 | OSASCRIPT_ENV_APP_PATH="/Applications/Support.app" osascript -l 'JavaScript' -e 'ObjC.import("LaunchServices"); $.LSRegisterURL($.NSURL.fileURLWithPath($.NSProcessInfo.processInfo.environment.objectForKey("OSASCRIPT_ENV_APP_PATH")), true)' &> /dev/null
57 |
58 | if [[ -n "${username}" ]]; then
59 | # Get the username ID
60 | uid=$(id -u "${username}")
61 |
62 | launchctl asuser "${uid}" sudo -u "${username}" open -na "${install_location}"
63 | fi
64 | fi
65 |
66 | # Add AssociatedBundleIdentifiers to show app name in Login Items on
67 | # macOS 13 and higher instead of developer name
68 | defaults write "/Library/LaunchAgents/${launch_agent}.plist" AssociatedBundleIdentifiers -array -string "nl.root3.support"
69 | # Set the Label and ProgramArguments
70 | defaults write "/Library/LaunchAgents/${launch_agent}.plist" Label -string "${launch_agent}"
71 | defaults write "/Library/LaunchAgents/${launch_agent}.plist" ProgramArguments -array -string "/Applications/Support.app/Contents/MacOS/Support"
72 | # Run every reboot
73 | defaults write "/Library/LaunchAgents/${launch_agent}.plist" KeepAlive -boolean yes
74 | # Set ProcessType to Interactive
75 | defaults write "/Library/LaunchAgents/${launch_agent}.plist" ProcessType -string "Interactive"
76 | # Set permissions
77 | chown root:wheel "/Library/LaunchAgents/${launch_agent}.plist"
78 | chmod 644 "/Library/LaunchAgents/${launch_agent}.plist"
79 |
80 | # Reload the LaunchAgent
81 | if [[ -n "${username}" ]]; then
82 |
83 | # Get the username ID
84 | uid=$(id -u "${username}")
85 |
86 | # Unload the LaunchAgent
87 | if launchctl print "gui/${uid}/${launch_agent}" &> /dev/null ; then
88 | launchctl bootout gui/${uid} "/Library/LaunchAgents/${launch_agent}.plist" &> /dev/null
89 | fi
90 |
91 | # Just to be sure, kill Support App if still running
92 | if pgrep -x "Support" ; then
93 | killall -9 "Support"
94 | fi
95 |
96 | # Load the LaunchAgent
97 | launchctl bootstrap gui/${uid} "/Library/LaunchAgents/${launch_agent}.plist"
98 | fi
99 |
100 | # ------------------ Uninstall SupportHelper ------------------
101 |
102 | # Remove SupportHelper and LaunchDaemon if found
103 | if [[ -f "/usr/local/bin/SupportHelper" ]]; then
104 |
105 | # Remove SupportHelper binary
106 | rm -f "/usr/local/bin/SupportHelper"
107 |
108 | # Unload the LaunchDaemon
109 | if launchctl print "system/${launch_daemon}" &> /dev/null ; then
110 | launchctl bootout "system/${launch_daemon}" &> /dev/null
111 | fi
112 |
113 | # Remove SupportHelper LaunchDaemon
114 | if [[ -f "/Library/LaunchDaemons/${launch_daemon}.plist" ]]; then
115 | rm -f "/Library/LaunchDaemons/${launch_daemon}.plist"
116 | fi
117 |
118 | fi
119 |
120 | # ------------------ PrivilegedHelperTool ------------------
121 |
122 | # Detect optional preference to opt-out of Privileged Helper Tool activation
123 | privileged_helper_tool_opt_out=$(/usr/bin/osascript -l JavaScript << EOS 2>/dev/null
124 | ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('nl.root3.support').objectForKey('DisablePrivilegedHelperTool'))
125 | EOS
126 | )
127 |
128 | # Setup Privileged Helper Tool if not opted out
129 | if [[ "${privileged_helper_tool_opt_out}" != "true" ]]; then
130 | "${install_location}/Contents/Resources/install_privileged_helper_tool.zsh"
131 | fi
--------------------------------------------------------------------------------
/src/Support.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Support.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Support.xcodeproj/xcshareddata/xcschemes/Support.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "Support-16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "Support-32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "Support-32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "Support-64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "Support-128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "Support-256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "Support-256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "Support-512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "Support-512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "Support-1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/AppIcon.appiconset/Support-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/AppIcon.appiconset/Support-1024.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/AppIcon.appiconset/Support-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/AppIcon.appiconset/Support-128.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/AppIcon.appiconset/Support-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/AppIcon.appiconset/Support-16.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/AppIcon.appiconset/Support-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/AppIcon.appiconset/Support-256.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/AppIcon.appiconset/Support-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/AppIcon.appiconset/Support-32.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/AppIcon.appiconset/Support-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/AppIcon.appiconset/Support-512.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/AppIcon.appiconset/Support-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/AppIcon.appiconset/Support-64.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/DefaultLogo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Support-128.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "Support-256.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/DefaultLogo.imageset/Support-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/DefaultLogo.imageset/Support-128.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/DefaultLogo.imageset/Support-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/root3nl/SupportApp/94c863984239a20e406d482c1bc51b2c74b16143/src/Support/Assets.xcassets/DefaultLogo.imageset/Support-256.png
--------------------------------------------------------------------------------
/src/Support/Assets.xcassets/hoverColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.000",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Support/Controllers/AppCatalogController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppCatalogController.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 | import os
10 | import SwiftUI
11 |
12 | class AppCatalogController: ObservableObject {
13 |
14 | // Unified Logging
15 | var logger = Logger(subsystem: "nl.root3.support", category: "AppCatalog")
16 |
17 | // Setup UserDefaults
18 | let defaults = UserDefaults(suiteName: "nl.root3.catalog")
19 |
20 | // App Catalog authorization code
21 | @AppStorage("authorization", store: UserDefaults(suiteName: "nl.root3.catalog")) var catalogAuthorization: String = ""
22 |
23 | // Get available app updates from App Catalog
24 | @AppStorage("Updates", store: UserDefaults(suiteName: "nl.root3.catalog")) var appUpdates: Int = 0
25 |
26 | // Get update interval
27 | @AppStorage("UpdateInterval", store: UserDefaults(suiteName: "nl.root3.catalog")) var updateInterval: Int = 0
28 |
29 | // Get last update date epoch
30 | @AppStorage("LastUpdated", store: UserDefaults(suiteName: "nl.root3.catalog.agent")) var lastUpdated: Int = 0
31 |
32 | // Current apps updating
33 | @Published var appsUpdating: [String] = []
34 |
35 | // Current apps in the queue
36 | @Published var appsQueued: [String] = []
37 |
38 | // Show app updates
39 | @Published var showAppUpdates: Bool = false
40 |
41 | // Array containing app details
42 | @Published var updateDetails: [InstalledAppItem] = []
43 |
44 | // Boolean to ignore a value change for "Updates" just once to avoid KVO in AppDelegate to trigger twice
45 | var ignoreUpdateChange: Bool = false
46 |
47 | // Calculate when next update schedule will run
48 | var nextUpdateDate: String {
49 | let fromDate = Date(timeIntervalSince1970: Double(lastUpdated))
50 | var toDate = fromDate.addingTimeInterval(Double(updateInterval) * 86400)
51 |
52 | // If next update is not in the future, show within the next hour
53 | // If next update if within on hour, show within the next hour as we don't know exactly when the LaunchDaemon will run
54 | if toDate < .now.addingTimeInterval(3600) {
55 | toDate = .now.addingTimeInterval(3600)
56 | }
57 |
58 | // Format the next update schedule in relative style
59 | var formatter = Date.RelativeFormatStyle()
60 | formatter.presentation = .numeric
61 | let relativeDate = toDate.formatted(formatter)
62 |
63 | return relativeDate
64 | }
65 |
66 | func getAppUpdates() {
67 |
68 | // Check available app updates
69 | logger.log("Checking app updates...")
70 |
71 | let command = "'/usr/local/bin/catalog --check-updates'"
72 |
73 | // Move to background thread
74 | DispatchQueue.global().async {
75 |
76 | // Setup XPC connection
77 | let connectionToService = NSXPCConnection(serviceName: "nl.root3.support.xpc")
78 | connectionToService.remoteObjectInterface = NSXPCInterface(with: SupportXPCProtocol.self)
79 | connectionToService.resume()
80 |
81 | // Run command when connection is successful. Run XPC synchronously and decode app updates once completed
82 | if let proxy = connectionToService.synchronousRemoteObjectProxyWithErrorHandler( { error in
83 | self.logger.error("\(error.localizedDescription, privacy: .public)")
84 | }) as? SupportXPCProtocol {
85 | proxy.executeScript(command: command) { exitCode in
86 |
87 | if exitCode == 0 {
88 | self.logger.log("Successfully checked app updates")
89 | } else {
90 | self.logger.error("Failed to check app updates")
91 | }
92 |
93 | }
94 | } else {
95 | self.logger.error("Failed to connect to SupportXPC service")
96 | }
97 |
98 | // Invalidate connection
99 | connectionToService.invalidate()
100 |
101 | // Decode app updates
102 | if let encodedAppUpdates = self.defaults?.object(forKey: "UpdateDetails") as? Data {
103 | let decoder = JSONDecoder()
104 | if let decodedAppUpdates = try? decoder.decode([InstalledAppItem].self, from: encodedAppUpdates) {
105 | DispatchQueue.main.async {
106 | self.logger.debug("Successfully decoded app updates")
107 | self.updateDetails = decodedAppUpdates
108 | }
109 | } else {
110 | self.logger.error("Failed to decode app updates: Invalid format")
111 | }
112 | } else {
113 | self.logger.error("Failed to decode app updates: Key 'UpdateDetails' does not exist")
114 | }
115 | }
116 |
117 | }
118 |
119 | // MARK: - Function to check if App Catalog is installed
120 | func catalogInstalled() -> Bool {
121 |
122 | let fileManager = FileManager.default
123 |
124 | // Path to app bundle
125 | let appURL = URL(fileURLWithPath: "/Applications/Catalog.app")
126 |
127 | // Path to binary symlink
128 | let cliURL = URL(fileURLWithPath: "/usr/local/bin/catalog")
129 |
130 | if fileManager.fileExists(atPath: appURL.path) && fileManager.fileExists(atPath: cliURL.path) && catalogAuthorization != "" {
131 | return true
132 | } else {
133 | return false
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/Support/Controllers/InstallTaskQueue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InstallTaskQueue.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 21/05/2025.
6 | //
7 |
8 | import Foundation
9 |
10 | class InstallTaskQueue {
11 |
12 | // Create singleton
13 | static let shared = InstallTaskQueue()
14 |
15 | // Create queue
16 | let queue = Queue()
17 |
18 | actor Queue {
19 | private var tasks: [(id: String, task: () async -> Void)] = []
20 | private var cancelledTaskIDs: Set = []
21 | private var isRunning = false
22 |
23 | func enqueue(id: String, _ task: @escaping () async -> Void) {
24 | tasks.append((id, task))
25 |
26 | // Check if current task is running before running the next task
27 | if !isRunning {
28 | isRunning = true
29 | Task {
30 | await runNext()
31 | }
32 | }
33 | }
34 |
35 | func cancel(_ id: String) {
36 | cancelledTaskIDs.insert(id)
37 | }
38 |
39 | // Run tasks
40 | private func runNext() async {
41 | while !tasks.isEmpty {
42 | let (id, task) = tasks.removeFirst()
43 | if !cancelledTaskIDs.contains(id) {
44 | await task()
45 | }
46 | cancelledTaskIDs.remove(id)
47 | }
48 | isRunning = false
49 | }
50 | }
51 |
52 | // Function to add new tasks
53 | func submit(id: String, task: @escaping () async -> Void) async {
54 | await queue.enqueue(id: id, task)
55 | }
56 |
57 | // Function to cancel task based on ID
58 | func cancel(taskID: String) async {
59 | await queue.cancel(taskID)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Support/EventMonitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EventMonitor.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 02/09/2020.
6 | //
7 |
8 | import Cocoa
9 | import os
10 |
11 | // Monitor mouse clicks
12 | public class EventMonitor {
13 |
14 | let logger = Logger(subsystem: "nl.root3.support", category: "EventMonitor")
15 |
16 | private var monitor: Any?
17 | private let mask: NSEvent.EventTypeMask
18 | private let handler: (NSEvent?) -> Void
19 |
20 | public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
21 | self.mask = mask
22 | self.handler = handler
23 | }
24 |
25 | deinit {
26 | stop()
27 | }
28 |
29 | public func start() {
30 | logger.debug("EventMonitor started")
31 | monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
32 | }
33 |
34 | public func stop() {
35 | if monitor != nil {
36 | logger.debug("EventMonitor stopped")
37 | NSEvent.removeMonitor(monitor!)
38 | monitor = nil
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Support/Extensions/RemoveEscapingCharacters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RemoveEscapingCharacters.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 13/05/2024.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - String extension to remove any escaping characters
11 | extension String {
12 |
13 | // Helper function to handle file paths and remove any escaping characters
14 | func removeEscapingCharacters() -> String {
15 | var newString = self
16 | newString = newString.replacingOccurrences(of: "\\ ", with: " ")
17 | return newString
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/Support/FileUtilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileUtilities.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 06/04/2024.
6 | //
7 |
8 | import Foundation
9 | import os
10 |
11 | class FileUtilities {
12 |
13 | let logger = Logger(subsystem: "nl.root3.support", category: "FileUtilities")
14 |
15 | // MARK: - Get script
16 | func getScriptPermissions(pathname: String) -> (ownerID: Int, permissions: NSNumber) {
17 | // Return the ID and permissions of the specified script
18 | var fileAttributes: [FileAttributeKey: Any]
19 | var ownerID: Int = 0
20 | var mode: NSNumber = 0
21 | do {
22 | fileAttributes = try FileManager.default.attributesOfItem(atPath: pathname.removeEscapingCharacters())
23 | if let ownerProperty = fileAttributes[.ownerAccountID] as? Int {
24 | ownerID = ownerProperty
25 | }
26 | if let modeProperty = fileAttributes[.posixPermissions] as? NSNumber {
27 | mode = modeProperty
28 | }
29 | } catch {
30 | logger.error("Could not read file at path \(pathname, privacy: .public)")
31 | }
32 | return (ownerID, mode)
33 | }
34 |
35 | // MARK: - Verify script is owned by root and permissions 755
36 | func verifyPermissions(pathname: String) -> Bool {
37 |
38 | let requiredPermissions: NSNumber = 0o755
39 |
40 | let (ownerID, mode) = getScriptPermissions(pathname: pathname)
41 |
42 | if ownerID == 0 && mode == requiredPermissions {
43 | logger.debug("Permissions for \(pathname, privacy: .public) are correct")
44 | return true
45 | } else {
46 | logger.error("Permissions for \(pathname, privacy: .public) are incorrect. Should be owned by root and with mode 755")
47 | }
48 | return false
49 | }
50 |
51 | // MARK: - Function to check if file or folder exists
52 | func fileOrFolderExists(path: String) -> Bool {
53 |
54 | let fileManager = FileManager.default
55 |
56 | // Path to app bundle
57 | let file = URL(fileURLWithPath: path)
58 |
59 | if fileManager.fileExists(atPath: file.path) {
60 | return true
61 | } else {
62 | return false
63 | }
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/Support/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 2.6.3
21 | CFBundleVersion
22 | 66
23 | LSApplicationCategoryType
24 | public.app-category.utilities
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | LSUIElement
28 |
29 | NSHumanReadableCopyright
30 | © 2025 Root3 B.V. All rights reserved.
31 | NSMainStoryboardFile
32 | Main
33 | NSPrincipalClass
34 | NSApplication
35 | SMPrivilegedExecutables
36 |
37 | nl.root3.support.helper
38 | anchor apple generic and identifier "nl.root3.support.helper" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "98LJ4XBGYK")
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/Support/Models/KerberosSSOExtensionModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KerberosSSOExtensionModel.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 31/10/2021.
6 | //
7 |
8 | import Foundation
9 |
10 | struct KerberosSSOExtension: Codable {
11 |
12 | let passwordExpiresDate: Date?
13 | let userName: String?
14 | let networkAvailable: String?
15 |
16 | enum CodingKeys: String, CodingKey {
17 | case passwordExpiresDate = "password_expires_date"
18 | case userName = "user_name"
19 | case networkAvailable = "networkAvailable"
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/Support/Models/SoftwareUpdateDeclarationModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoftwareUpdateDeclarationModel.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 06/03/2024.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Declaration: Codable {
11 | let detailsURL: String?
12 | let targetBuildVersion: String?
13 | let targetLocalDateTime: String
14 | let targetOSVersion: String
15 |
16 | enum CodingKeys: String, CodingKey {
17 | case detailsURL = "DetailsURL"
18 | case targetBuildVersion = "TargetBuildVersion"
19 | case targetLocalDateTime = "TargetLocalDateTime"
20 | case targetOSVersion = "TargetOSVersion"
21 | }
22 | }
23 |
24 | struct PolicyFields: Codable {
25 | let declarations: [String: Declaration]?
26 |
27 | enum CodingKeys: String, CodingKey {
28 | case declarations = "Declarations"
29 | }
30 |
31 | }
32 |
33 | struct SoftwareUpdateDeclarationModel: Codable {
34 | let policyFields: PolicyFields
35 |
36 | enum CodingKeys: String, CodingKey {
37 | case policyFields = "SUCorePersistedStatePolicyFields"
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/src/Support/Models/SoftwareUpdateModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoftwareUpdateModel.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 18/12/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | struct SoftwareUpdateModel: Identifiable, Codable, Hashable {
11 |
12 | var id: String
13 | var displayName: String
14 | var displayVersion: String?
15 | var mobileSoftwareUpdate: Bool?
16 | var productKey: String?
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case id = "Identifier"
20 | case displayName = "Display Name"
21 | case displayVersion = "Display Version"
22 | case mobileSoftwareUpdate = "MobileSoftwareUpdate"
23 | case productKey = "Product Key"
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/Support/NotificationNames.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationNames.swift
3 | // NotificationNames
4 | //
5 | // Created by Jordy Witteman on 11/10/2021.
6 | //
7 |
8 | import Foundation
9 |
10 | // Static notification names
11 | extension Notification.Name {
12 |
13 | static let uptimeDaysLimit = Notification.Name("UptimeDaysLimit")
14 | static let storageLimit = Notification.Name("StorageLimit")
15 | static let networkState = Notification.Name("NetworkState")
16 | static let passwordExpiryLimit = Notification.Name("PasswordExpiryLimit")
17 | static let recommendedUpdates = Notification.Name("RecommendedUpdates")
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/Support/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/Support/PrivilegedHelper/ExecutionService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExecutionService.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ExecutionService {
11 |
12 | static func executeScript(command: String, completion: @escaping (NSNumber) -> Void) throws {
13 | let remote = try HelperRemote().getRemote()
14 |
15 | remote.executeScript(command: command) { (exitCode) in
16 | completion(exitCode)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Support/PrivilegedHelper/HelperRemote.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelperRemote.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 | import os
10 | import XPC
11 | import ServiceManagement
12 |
13 | struct HelperRemote {
14 |
15 | var logger = Logger(subsystem: "nl.root3.support", category: "SupportHelper")
16 |
17 | // MARK: - Properties
18 | var isHelperInstalled: Bool { FileManager.default.fileExists(atPath: HelperConstants.helperPath) }
19 |
20 | // MARK: - Functions
21 | /// Install the Helper in the privileged helper tools folder and load the daemon
22 | func installHelper() throws {
23 |
24 | // try to get a valid empty authorisation
25 | var authRef: AuthorizationRef?
26 | var authStatus = AuthorizationCreate(nil, nil, [.preAuthorize], &authRef)
27 |
28 | guard authStatus == errAuthorizationSuccess else {
29 | logger.error("Unable to get a valid empty authorization reference to load Helper daemon")
30 | throw PrivilegedHelperError.helperInstallation("Unable to get a valid empty authorization reference to load Helper daemon")
31 | }
32 |
33 | // create an AuthorizationItem to specify we want to bless a privileged Helper
34 | let authItem = kSMRightBlessPrivilegedHelper.withCString { authorizationString in
35 | AuthorizationItem(name: authorizationString, valueLength: 0, value: nil, flags: 0)
36 | }
37 |
38 | // it's required to pass a pointer to the call of the AuthorizationRights.init function
39 | let pointer = UnsafeMutablePointer.allocate(capacity: 1)
40 | pointer.initialize(to: authItem)
41 |
42 | defer {
43 | // as we instantiate a pointer, it's our responsibility to make sure it's deallocated
44 | pointer.deinitialize(count: 1)
45 | pointer.deallocate()
46 | }
47 |
48 | // store the authorization items inside an AuthorizationRights object
49 | var authRights = AuthorizationRights(count: 1, items: pointer)
50 |
51 | let flags: AuthorizationFlags = [.interactionAllowed, .extendRights, .preAuthorize]
52 | authStatus = AuthorizationCreate(&authRights, nil, flags, &authRef)
53 |
54 | guard authStatus == errAuthorizationSuccess else {
55 | logger.error("Unable to get a valid loading authorization reference to load Helper daemon")
56 | throw PrivilegedHelperError.helperInstallation("Unable to get a valid loading authorization reference to load Helper daemon")
57 | }
58 |
59 | // Try to install the helper and to load the daemon with authorization
60 | var error: Unmanaged?
61 | if SMJobBless(kSMDomainSystemLaunchd, HelperConstants.domain as CFString, authRef, &error) == false {
62 | let blessError = error!.takeRetainedValue() as Error
63 | logger.error("Error while installing the Helper: \(blessError.localizedDescription, privacy: .public)")
64 | throw PrivilegedHelperError.helperInstallation("Error while installing the Helper: \(blessError.localizedDescription)")
65 | }
66 |
67 | // Helper successfully installed
68 | // Release the authorization, as mentioned in the doc
69 | AuthorizationFree(authRef!, [])
70 | }
71 |
72 | private func createConnection() -> NSXPCConnection {
73 | let connection = NSXPCConnection(machServiceName: HelperConstants.domain, options: .privileged)
74 | connection.remoteObjectInterface = NSXPCInterface(with: SupportHelperProtocol.self)
75 | connection.exportedInterface = NSXPCInterface(with: RemoteApplicationProtocol.self)
76 | connection.exportedObject = self
77 |
78 | connection.invalidationHandler = { [isHelperInstalled] in
79 | if isHelperInstalled {
80 | logger.error("Unable to connect to Helper although it is installed")
81 | } else {
82 | logger.error("Helper is not installed")
83 | }
84 | }
85 | logger.debug("Helper connected!")
86 |
87 | connection.resume()
88 |
89 | return connection
90 | }
91 |
92 | private func getConnection() throws -> NSXPCConnection {
93 | if !isHelperInstalled {
94 | // we'll try to install the Helper if not already installed, but we need to get the admin authorization
95 | try installHelper()
96 | }
97 | return createConnection()
98 | }
99 |
100 | func getRemote() throws -> SupportHelperProtocol {
101 | var proxyError: Error?
102 |
103 | // Try to get the helper
104 | let helper = try getConnection().remoteObjectProxyWithErrorHandler({ (error) in
105 | proxyError = error
106 | }) as? SupportHelperProtocol
107 |
108 | // Try to unwrap the Helper
109 | if let unwrappedHelper = helper {
110 | return unwrappedHelper
111 | } else {
112 | logger.error("\(proxyError?.localizedDescription ?? "Unknown error")")
113 | throw PrivilegedHelperError.helperConnection(proxyError?.localizedDescription ?? "Unknown error")
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/Support/Scripts/install_privileged_helper_tool.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Install Privileged Helper Tool
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # This script will install the Privileged Helper Tool.
9 | #
10 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
11 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
12 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
13 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
14 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
15 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | # ------------------ edit the variables below this line ------------------
18 |
19 | # Path to Privileged Helper Tool
20 | privileged_helper_tool="/Library/PrivilegedHelperTools/nl.root3.support.helper"
21 |
22 | # LaunchDaemon domain
23 | launch_daemon="nl.root3.support.helper"
24 |
25 | # Install location
26 | install_location="/Applications/Support.app"
27 |
28 | # ------------------ PrivilegedHelperTool ------------------
29 |
30 | # Create "/Library/PrivilegedHelperTools/" if not present
31 | if [[ ! -d "/Library/PrivilegedHelperTools/" ]]; then
32 | mkdir "/Library/PrivilegedHelperTools/"
33 | fi
34 |
35 | # Copy the PrivilegedHelperTool
36 | cp "${install_location}/Contents/Library/LaunchServices/${launch_daemon}" "/Library/PrivilegedHelperTools/"
37 | # Set permissions
38 | chown root:wheel "${privileged_helper_tool}"
39 | chmod 544 "${privileged_helper_tool}"
40 |
41 | # ------------------ LaunchDaemon PrivilegedHelperTool ------------------
42 |
43 | # Add AssociatedBundleIdentifiers to show app name in Login Items on
44 | # macOS 13 and higher instead of developer name
45 | defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" AssociatedBundleIdentifiers -array -string "nl.root3.support"
46 | # Set the Label and ProgramArguments
47 | defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" Label -string "${launch_daemon}"
48 | defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" ProgramArguments -array -string "${privileged_helper_tool}"
49 | # Set MachServices
50 | defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" MachServices -dict -string "nl.root3.support.helper" -bool true
51 | # Set permissions
52 | chown root:wheel "/Library/LaunchDaemons/${launch_daemon}.plist"
53 | chmod 644 "/Library/LaunchDaemons/${launch_daemon}.plist"
54 |
55 | # Unload the LaunchDaemon
56 | if launchctl print "system/${launch_daemon}" &> /dev/null ; then
57 | launchctl bootout "system/${launch_daemon}" &> /dev/null
58 | fi
59 |
60 | # Load the LaunchDaemon
61 | if ! launchctl print "system/${launch_daemon}" &> /dev/null ; then
62 | launchctl bootstrap system "/Library/LaunchDaemons/${launch_daemon}.plist"
63 | fi
--------------------------------------------------------------------------------
/src/Support/Scripts/uninstall_privileged_helper_tool.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh --no-rcs
2 |
3 | # Uninstall Privileged Helper Tool
4 | #
5 | #
6 | # Copyright 2024 Root3 B.V. All rights reserved.
7 | #
8 | # This script will uninstall the Privileged Helper Tool.
9 | #
10 | # THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
11 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
12 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
13 | # EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
14 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
15 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | # ------------------ edit the variables below this line ------------------
18 |
19 | # Path to Privileged Helper Tool
20 | privileged_helper_tool="/Library/PrivilegedHelperTools/nl.root3.support.helper"
21 |
22 | # LaunchDaemon domain
23 | launch_daemon="nl.root3.support.helper"
24 |
25 | # Remove Privileged Helper Tool
26 | if [[ -f "${privileged_helper_tool}" ]]; then
27 | rm -f "${privileged_helper_tool}"
28 | fi
29 |
30 | # Unload LaunchDaemon
31 | if launchctl print "system/${launch_daemon}" &> /dev/null ; then
32 | launchctl bootout "system/${launch_daemon}" &> /dev/null
33 | fi
34 |
35 | # Remove LaunchDaemon
36 | if [[ -f "/Library/LaunchDaemons/${launch_daemon}.plist" ]]; then
37 | rm -f "/Library/LaunchDaemons/${launch_daemon}.plist"
38 | fi
--------------------------------------------------------------------------------
/src/Support/Support.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 | com.apple.security.temporary-exception.shared-preference.read-only
10 |
11 | nl.root3.catalog
12 | nl.root3.catalog.agent
13 | com.apple.applicationaccess
14 | com.trusourcelabs.NoMAD
15 | com.apple.SoftwareUpdate
16 | com.jamf.connect.state
17 |
18 | com.apple.security.temporary-exception.mach-lookup.global-name
19 |
20 | nl.root3.support.helper
21 |
22 | com.apple.security.temporary-exception.apple-events
23 |
24 | com.apple.systemevents
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/Support/Views/AppCatalog/InstalledAppItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InstalledAppItem.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 21/10/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct InstalledAppItem: Codable, Identifiable, Hashable {
11 |
12 | let id: String
13 | let name: String?
14 | let icon: String?
15 | let version: String?
16 | let newVersion: String?
17 | }
18 |
--------------------------------------------------------------------------------
/src/Support/Views/AppView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 17/05/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AppView: View {
11 |
12 | // Access AppDelegate
13 | @EnvironmentObject private var appDelegate: AppDelegate
14 |
15 | @EnvironmentObject var computerinfo: ComputerInfo
16 | @EnvironmentObject var userinfo: UserInfo
17 | @EnvironmentObject var preferences: Preferences
18 | @EnvironmentObject var appCatalogController: AppCatalogController
19 |
20 | // Dark Mode detection
21 | @Environment(\.colorScheme) var colorScheme
22 |
23 | // Simple property wrapper boolean to visualize data loading when app opens
24 | @State var placeholdersEnabled = true
25 |
26 | // Version and build number
27 | var version = Bundle.main.infoDictionary!["CFBundleShortVersionString"]! as! String
28 | var build = Bundle.main.infoDictionary!["CFBundleVersion"]! as! String
29 |
30 | var body: some View {
31 |
32 | // MARK: - ZStack with blur effect
33 | ZStack {
34 | EffectsView(material: NSVisualEffectView.Material.fullScreenUI, blendingMode: NSVisualEffectView.BlendingMode.behindWindow)
35 |
36 | // We need to provide Quit option for Apple App Review approval
37 | if !preferences.hideQuit {
38 | QuitButton()
39 | }
40 |
41 | // Show "Beta" in the top left corner for beta releases
42 | if preferences.betaRelease {
43 | HStack {
44 |
45 | VStack {
46 | Text("Beta release \(version) (\(build))")
47 | .font(.system(.subheadline, design: .rounded))
48 | .opacity(0.5)
49 |
50 | Spacer()
51 | }
52 |
53 | Spacer()
54 | }
55 | .padding(.leading, 16.0)
56 | .padding(.top, 10)
57 | }
58 |
59 | VStack(spacing: 10) {
60 |
61 | // MARK: - Horizontal stack with Title and Logo
62 | HeaderView()
63 |
64 | if preferences.showWelcomeScreen && !preferences.hasSeenWelcomeScreen {
65 | WelcomeView()
66 | } else {
67 | if appCatalogController.showAppUpdates {
68 | AppUpdatesView()
69 | } else if computerinfo.showMacosUpdates {
70 | UpdateView()
71 | } else if computerinfo.showUptimeAlert {
72 | UptimeAlertView()
73 | } else {
74 | ContentView()
75 | }
76 | }
77 |
78 | // MARK: - Footnote
79 | if preferences.footerText != "" {
80 | HStack {
81 |
82 |
83 | // Supports for markdown through a variable:
84 | // https://blog.eidinger.info/3-surprises-when-using-markdown-in-swiftui
85 | Text(.init(preferences.footerText.replaceLocalVariables(computerInfo: computerinfo, userInfo: userinfo)))
86 | .font(.system(.subheadline, design: .rounded))
87 | .foregroundColor(colorScheme == .dark ? .white.opacity(0.5) : .black.opacity(0.5))
88 | .textSelection(.enabled)
89 |
90 | Spacer()
91 |
92 | }
93 | .padding(.horizontal, 10)
94 | // Workaround to support multiple lines
95 | .frame(minWidth: 382, idealWidth: 382, maxWidth: 382)
96 | .fixedSize()
97 | }
98 | }
99 | .padding(.bottom, 10)
100 | }
101 | // Set default popover width
102 | .frame(minWidth: 382, idealWidth: 382, maxWidth: 382)
103 | // MARK: - Run functions when ContentView appears for the first time
104 | .onAppear {
105 | dataLoadingEffect()
106 | }
107 | // MARK: - Show placeholders while loading
108 | .redacted(reason: placeholdersEnabled ? .placeholder : .init())
109 | }
110 |
111 | // MARK: - Start app with placeholders and show data after 0.4 seconds to visualize data loading.
112 | func dataLoadingEffect() {
113 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
114 | placeholdersEnabled = false
115 | }
116 | }
117 | }
118 |
119 | struct AppView_Previews: PreviewProvider {
120 | static var previews: some View {
121 | AppView()
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonTemplateViews/InfoItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoItem.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 30/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoItem: View {
11 | var title: String
12 | var subtitle: String
13 | var image: String
14 | var symbolColor: Color
15 | var notificationBadge: Int?
16 | var notificationBadgeBool: Bool?
17 | var loading: Bool?
18 |
19 | // Vars to activate hover effect
20 | @State var hoverEffectEnable: Bool
21 | @State var hoverView = false
22 |
23 | var body: some View {
24 |
25 | ZStack {
26 |
27 | HStack {
28 | if loading ?? false {
29 | Ellipse()
30 | .foregroundColor(Color.gray.opacity(0.5))
31 | .overlay(
32 | ProgressView()
33 | .scaleEffect(0.5)
34 | )
35 | .frame(width: 26, height: 26)
36 | .padding(.leading, 10)
37 | } else {
38 | Ellipse()
39 | .foregroundColor((hoverView && hoverEffectEnable) ? .primary : symbolColor)
40 | .overlay(
41 | Image(systemName: image)
42 | .foregroundColor((hoverView && hoverEffectEnable) ? Color("hoverColor") : Color.white)
43 | )
44 | .frame(width: 26, height: 26)
45 | .padding(.leading, 10)
46 | }
47 |
48 | VStack(alignment: .leading) {
49 | Text(title).font(.system(.body, design: .rounded)).fontWeight(.medium)
50 | .lineLimit(2)
51 |
52 | Text(subtitle).font(.system(.subheadline, design: .rounded))
53 | .lineLimit(2)
54 | }
55 | Spacer()
56 | }
57 |
58 | // Optionally show notification badge with counter
59 | if notificationBadge != nil && notificationBadge! > 0 {
60 | NotificationBadgeView(badgeCounter: notificationBadge!)
61 | }
62 |
63 | // Optionally show notification badge with warning
64 | if notificationBadgeBool ?? false {
65 | NotificationBadgeTextView(badgeCounter: "!")
66 | }
67 |
68 | }
69 | .frame(width: 176, height: 60)
70 | .background(hoverView && hoverEffectEnable ? EffectsView(material: NSVisualEffectView.Material.windowBackground, blendingMode: NSVisualEffectView.BlendingMode.withinWindow) : EffectsView(material: NSVisualEffectView.Material.popover, blendingMode: NSVisualEffectView.BlendingMode.withinWindow))
71 | .cornerRadius(10)
72 | // Apply gray and black border in Dark Mode to better view the buttons like Control Center
73 | .modifier(DarkModeBorder())
74 | .shadow(color: Color.black.opacity(0.2), radius: 4, y: 2)
75 | .onHover() {
76 | hover in self.hoverView = hover
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/AppCatalogSubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppCatalogSubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 18/10/2023.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AppCatalogSubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get App Catalog information
16 | @EnvironmentObject var appCatalogController: AppCatalogController
17 |
18 | // Get preferences or default values
19 | @StateObject var preferences = Preferences()
20 |
21 | // Make UserDefaults easy to use
22 | let defaults = UserDefaults.standard
23 |
24 | // Dark Mode detection
25 | @Environment(\.colorScheme) var colorScheme
26 |
27 | // Boolean to show AppUpdatesView as popover
28 | @State var showAppCatalogPopover: Bool = false
29 |
30 | // Set the custom color for all symbols depending on Light or Dark Mode.
31 | var customColor: String {
32 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
33 | return preferences.customColor
34 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
35 | return preferences.customColorDarkMode
36 | } else {
37 | return preferences.customColor
38 | }
39 | }
40 |
41 | var updatesString: String {
42 | if !appCatalogController.catalogInstalled() {
43 | return NSLocalizedString("APP_CATALOG_NOT_CONFIGURED", comment: "")
44 | } else {
45 | if appCatalogController.appUpdates > 0 {
46 | return NSLocalizedString("UPDATES_AVAILABLE", comment: "")
47 | } else {
48 | return NSLocalizedString("UP_TO_DATE", comment: "")
49 | }
50 | }
51 | }
52 |
53 | var body: some View {
54 |
55 | InfoItem(title: NSLocalizedString("APPS", comment: ""), subtitle: updatesString, image: "arrow.down.app.fill", symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadge: appCatalogController.appUpdates, notificationBadgeBool: appCatalogController.catalogInstalled() ? false : true, loading: appCatalogController.appsUpdating.isEmpty ? false : true, hoverEffectEnable: true)
56 | .modify {
57 | if #available(macOS 13, *) {
58 | $0.onTapGesture {
59 | self.appCatalogController.showAppUpdates.toggle()
60 | }
61 | } else {
62 | $0.onTapGesture {
63 | showAppCatalogPopover.toggle()
64 | }
65 | }
66 | }
67 | // Legacy popover for macOS 12
68 | .popover(isPresented: $showAppCatalogPopover, arrowEdge: .leading) {
69 | AppUpdatesView()
70 | }
71 | }
72 |
73 | }
74 |
75 | //#Preview {
76 | // AppCatalogSubview()
77 | //}
78 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/ComputerNameSubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComputerNameSubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 17/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ComputerNameSubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Set the custom color for all symbols depending on Light or Dark Mode.
25 | var customColor: String {
26 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
27 | return preferences.customColor
28 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
29 | return preferences.customColorDarkMode
30 | } else {
31 | return preferences.customColor
32 | }
33 | }
34 |
35 | // Link to About This Mac
36 | var aboutLink: String {
37 | if #available(macOS 13, *) {
38 | return "x-apple.systempreferences:com.apple.SystemProfiler.AboutExtension"
39 | } else {
40 | return "com.apple.AboutThisMacLauncher"
41 | }
42 | }
43 |
44 | // Link type for About This Mac
45 | var aboutLinkType: String {
46 | if #available(macOS 13, *) {
47 | return "URL"
48 | } else {
49 | return "App"
50 | }
51 | }
52 |
53 | var body: some View {
54 |
55 | // InfoItem(title: NSLocalizedString("Computer Name", comment: ""), subtitle: computerinfo.hostname, image: computerinfo.computerNameIcon, symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), hoverEffectEnable: false)
56 | ItemDouble(title: NSLocalizedString("Computer Name", comment: ""), secondTitle: NSLocalizedString("Model", comment: ""), subtitle: computerinfo.hostname, secondSubtitle: computerinfo.modelNameString, linkType: aboutLinkType, link: aboutLink, image: computerinfo.computerNameIcon, symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), hoverEffectEnable: true)
57 |
58 | }
59 | }
60 |
61 | struct ComputerNameSubview_Previews: PreviewProvider {
62 | static var previews: some View {
63 | ComputerNameSubview()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/ExtensionASubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtensionASubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 13/11/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ExtensionASubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Set the custom color for all symbols depending on Light or Dark Mode.
25 | var customColor: String {
26 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
27 | return preferences.customColor
28 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
29 | return preferences.customColorDarkMode
30 | } else {
31 | return preferences.customColor
32 | }
33 | }
34 |
35 | var body: some View {
36 |
37 | Item(title: preferences.extensionTitleA, subtitle: preferences.extensionValueA, linkType: preferences.extensionTypeA, link: preferences.extensionLinkA, image: preferences.extensionSymbolA, symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadgeBool: preferences.extensionAlertA, loading: preferences.extensionLoadingA, linkPrefKey: Preferences.extensionLinkAKey, hoverEffectEnable: true, hoverView: false, animate: false)
38 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/ExtensionBSubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtensionBSubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 13/11/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ExtensionBSubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Set the custom color for all symbols depending on Light or Dark Mode.
25 | var customColor: String {
26 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
27 | return preferences.customColor
28 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
29 | return preferences.customColorDarkMode
30 | } else {
31 | return preferences.customColor
32 | }
33 | }
34 |
35 | var body: some View {
36 |
37 | Item(title: preferences.extensionTitleB, subtitle: preferences.extensionValueB, linkType: preferences.extensionTypeB, link: preferences.extensionLinkB, image: preferences.extensionSymbolB, symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadgeBool: preferences.extensionAlertB, loading: preferences.extensionLoadingB, linkPrefKey: Preferences.extensionLinkBKey, hoverEffectEnable: true, hoverView: false, animate: false)
38 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/MacOSVersionSubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MacOSVersionSubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 17/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MacOSVersionSubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Boolean to show UpdateViewLegacy as popover
25 | @State var showUpdatePopover: Bool = false
26 |
27 | // Set the custom color for all symbols depending on Light or Dark Mode.
28 | var customColor: String {
29 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
30 | return preferences.customColor
31 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
32 | return preferences.customColorDarkMode
33 | } else {
34 | return preferences.customColor
35 | }
36 | }
37 |
38 | var body: some View {
39 |
40 | InfoItem(title: "macOS \(computerinfo.macOSVersionName)", subtitle: computerinfo.macOSVersion, image: "applelogo", symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadge: computerinfo.recommendedUpdates.count, hoverEffectEnable: true)
41 | .modify {
42 | if #available(macOS 13, *) {
43 | $0.onTapGesture {
44 | computerinfo.showMacosUpdates.toggle()
45 | }
46 | } else {
47 | $0.onTapGesture {
48 | showUpdatePopover.toggle()
49 | }
50 | }
51 | }
52 | // Legacy popover for macOS 12
53 | .popover(isPresented: $showUpdatePopover, arrowEdge: .leading) {
54 | UpdateViewLegacy(updateCounter: computerinfo.recommendedUpdates.count, color: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor))
55 | }
56 | }
57 | }
58 |
59 | struct MacOSVersionSubview_Previews: PreviewProvider {
60 | static var previews: some View {
61 | MacOSVersionSubview()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/ModelSubView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelSubView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 29/04/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ModelSubView: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Set the custom color for all symbols depending on Light or Dark Mode.
25 | var customColor: String {
26 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
27 | return preferences.customColor
28 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
29 | return preferences.customColorDarkMode
30 | } else {
31 | return preferences.customColor
32 | }
33 | }
34 |
35 | var body: some View {
36 |
37 | Item(title: "Model", subtitle: "\(computerinfo.modelShortName) \(computerinfo.modelYear)", linkType: "App", link: "com.apple.AboutThisMacLauncher", image: computerinfo.computerNameIcon, symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), hoverEffectEnable: true, animate: false)
38 |
39 | }
40 |
41 | }
42 |
43 | struct ModelSubView_Previews: PreviewProvider {
44 | static var previews: some View {
45 | ModelSubView()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/NetworkSubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkSubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 17/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NetworkSubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Set the custom color for all symbols depending on Light or Dark Mode.
25 | var customColor: String {
26 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
27 | return preferences.customColor
28 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
29 | return preferences.customColorDarkMode
30 | } else {
31 | return preferences.customColor
32 | }
33 | }
34 |
35 | // Link Network Preference Pane
36 | var networkLink: String {
37 | if #available(macOS 13, *) {
38 | return "x-apple.systempreferences:com.apple.Network-Settings.extension"
39 | } else {
40 | return "open /System/Library/PreferencePanes/Network.prefPane"
41 | }
42 | }
43 |
44 | // Link type for Network Preference Pane
45 | var networkLinkType: String {
46 | if #available(macOS 13, *) {
47 | return "URL"
48 | } else {
49 | return "Command"
50 | }
51 | }
52 |
53 | var body: some View {
54 |
55 | Item(title: computerinfo.networkName, subtitle: computerinfo.ipAddress, linkType: networkLinkType, link: networkLink, image: computerinfo.networkInterfaceSymbol, symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadgeBool: computerinfo.selfSignedIP, hoverEffectEnable: true, hoverView: false, animate: false)
56 |
57 | }
58 | }
59 |
60 | struct NetworkSubview_Previews: PreviewProvider {
61 | static var previews: some View {
62 | NetworkSubview()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/PasswordSubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PasswordSubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 16/05/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PasswordSubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get user info from functions in class
16 | @EnvironmentObject var userinfo: UserInfo
17 |
18 | // Get preferences or default values
19 | @StateObject var preferences = Preferences()
20 |
21 | // Make UserDefaults easy to use
22 | let defaults = UserDefaults.standard
23 |
24 | // FIXME: - Remove when Jamf Connect Password Change can be triggered
25 | // https://docs.jamf.com/jamf-connect/2.9.1/documentation/Jamf_Connect_URL_Scheme.html#ID-00005c31
26 | // Set preference suite to "com.jamf.connect.state"
27 | let defaultsJamfConnect = UserDefaults(suiteName: "com.jamf.connect.state")
28 |
29 | // Dark Mode detection
30 | @Environment(\.colorScheme) var colorScheme
31 |
32 | // Set the custom color for all symbols depending on Light or Dark Mode.
33 | var customColor: String {
34 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
35 | return preferences.customColor
36 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
37 | return preferences.customColorDarkMode
38 | } else {
39 | return preferences.customColor
40 | }
41 | }
42 |
43 | // Link type for Password item
44 | var linkType: String {
45 | if preferences.passwordType == "Apple" {
46 | if #available(macOS 13, *) {
47 | return "URL"
48 | } else {
49 | return "Command"
50 | }
51 | } else if preferences.passwordType == "KerberosSSO" {
52 | if userinfo.networkUnavailable {
53 | return "KerberosSSOExtensionUnavailable"
54 | } else {
55 | return "Command"
56 | }
57 | } else if preferences.passwordType == "Nomad" {
58 | return "Command"
59 | } else if preferences.passwordType == "JamfConnect" {
60 | // FIXME: - Remove when Jamf Connect Password Change can be triggered
61 | // https://docs.jamf.com/jamf-connect/2.9.1/documentation/Jamf_Connect_URL_Scheme.html#ID-00005c31
62 | if defaultsJamfConnect?.bool(forKey: "PasswordCurrent") ?? false {
63 | return "JamfConnectPasswordChangeException"
64 | } else {
65 | return "Command"
66 | }
67 | } else {
68 | if #available(macOS 13, *) {
69 | return "URL"
70 | } else {
71 | return "Command"
72 | }
73 | }
74 | }
75 |
76 | var body: some View {
77 |
78 | // Item(title: "Mac " + NSLocalizedString("Password", comment: ""), subtitle: userinfo.userPasswordExpiryString, linkType: "Command", link: "open /System/Library/PreferencePanes/Accounts.prefPane", image: "key.fill", symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadgeBool: userinfo.passwordExpiryLimitReached, hoverEffectEnable: true, animate: false)
79 |
80 | // Option to show another subtitle offering to change the local Mac password
81 |
82 | ItemDouble(title: preferences.passwordLabel, secondTitle: preferences.passwordLabel, subtitle: userinfo.userPasswordExpiryString, secondSubtitle: userinfo.passwordChangeString, linkType: linkType, link: userinfo.passwordChangeLink, image: "key.fill", symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadgeBool: userinfo.passwordExpiryLimitReached, hoverEffectEnable: true)
83 |
84 | // Expirimental view with link to password change view
85 |
86 | // InfoItem(title: "Mac " + NSLocalizedString("Password", comment: ""), subtitle: userinfo.passwordString, image: "key.fill", symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadge: userinfo.passwordExpiryLimitReached, hoverEffectEnable: true)
87 | // .onTapGesture {
88 | // computerinfo.showPasswordChange.toggle()
89 | // }
90 |
91 | }
92 | }
93 |
94 | struct PasswordSubview_Previews: PreviewProvider {
95 | static var previews: some View {
96 | PasswordSubview()
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/StorageSubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StorageSubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 17/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct StorageSubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Set the custom color for all symbols depending on Light or Dark Mode.
25 | var customColor: String {
26 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
27 | return preferences.customColor
28 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
29 | return preferences.customColorDarkMode
30 | } else {
31 | return preferences.customColor
32 | }
33 | }
34 |
35 | var body: some View {
36 |
37 | ProgressBarItem(percentageUsed: "\(computerinfo.capacityPercentageRounded)% " + NSLocalizedString("Used", comment: ""), storageAvailable: "\(computerinfo.capacityRounded) " + NSLocalizedString("Available", comment: ""), image: "internaldrive.fill", symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadgeBool: computerinfo.storageLimitReached, percentage: computerinfo.capacityPercentage, hoverEffectEnable: true)
38 |
39 | }
40 | }
41 |
42 | struct StorageSubview_Previews: PreviewProvider {
43 | static var previews: some View {
44 | StorageSubview()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Support/Views/ButtonViews/UptimeSubview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UptimeSubview.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 17/03/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct UptimeSubview: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Boolean to show legacy uptime alert when clicked
25 | @State var uptimeAlert: Bool = false
26 |
27 | // Set the custom color for all symbols depending on Light or Dark Mode.
28 | var customColor: String {
29 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
30 | return preferences.customColor
31 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
32 | return preferences.customColorDarkMode
33 | } else {
34 | return preferences.customColor
35 | }
36 | }
37 |
38 | // Enable hover effect and tap gesture when UptimeDaysLimit is configured
39 | var hoverEffectEnabled: Bool {
40 | if preferences.uptimeDaysLimit > 0 {
41 | return true
42 | } else {
43 | return false
44 | }
45 | }
46 |
47 | // Set different legacy alert text when UptimeDaysLimit is set to 1
48 | var alertText: String {
49 | if preferences.uptimeDaysLimit > 1 {
50 | return NSLocalizedString("ADMIN_RECOMMENDS_RESTARTING_EVERY", comment: "") + " \(preferences.uptimeDaysLimit)" + NSLocalizedString(" days", comment: "")
51 | } else {
52 | return NSLocalizedString("ADMIN_RECOMMENDS_RESTARTING_EVERY_DAY", comment: "")
53 | }
54 | }
55 |
56 | var body: some View {
57 |
58 | if hoverEffectEnabled {
59 | InfoItem(title: NSLocalizedString("Last Reboot", comment: ""), subtitle: "\(computerinfo.uptimeRounded) \(computerinfo.uptimeText) " + NSLocalizedString("ago", comment: ""), image: "clock.fill", symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadgeBool: computerinfo.uptimeLimitReached, hoverEffectEnable: true)
60 | .modify {
61 | if #available(macOS 13, *) {
62 | $0.onTapGesture {
63 | if hoverEffectEnabled {
64 | computerinfo.showUptimeAlert.toggle()
65 | }
66 | }
67 | } else {
68 | $0.onTapGesture {
69 | if hoverEffectEnabled {
70 | uptimeAlert.toggle()
71 | }
72 | }
73 | }
74 | }
75 | // Legacy popover for macOS 12
76 | .popover(isPresented: $uptimeAlert, arrowEdge: .leading) {
77 | PopoverAlertView(uptimeAlert: $uptimeAlert, title: NSLocalizedString("RESTART_REGULARLY", comment: ""), message: alertText)
78 | }
79 | } else {
80 | InfoItem(title: NSLocalizedString("Last Reboot", comment: ""), subtitle: "\(computerinfo.uptimeRounded) \(computerinfo.uptimeText) " + NSLocalizedString("ago", comment: ""), image: "clock.fill", symbolColor: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor), notificationBadgeBool: false, hoverEffectEnable: false)
81 | }
82 | }
83 | }
84 |
85 | struct UptimeSubview_Previews: PreviewProvider {
86 | static var previews: some View {
87 | UptimeSubview()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Support/Views/EffectsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EffectsView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 30/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Blur Effect
11 | struct EffectsView: NSViewRepresentable {
12 | var material: NSVisualEffectView.Material
13 | var blendingMode: NSVisualEffectView.BlendingMode
14 |
15 | func makeNSView(context: Context) -> NSVisualEffectView {
16 | let visualEffectView = NSVisualEffectView()
17 | visualEffectView.material = material
18 | visualEffectView.blendingMode = blendingMode
19 | visualEffectView.state = NSVisualEffectView.State.active
20 | return visualEffectView
21 | }
22 |
23 | func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) {
24 | visualEffectView.material = material
25 | visualEffectView.blendingMode = blendingMode
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Support/Views/HeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeaderView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 11/07/2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HeaderView: View {
11 |
12 | // Get computer info from functions in class
13 | @EnvironmentObject var computerinfo: ComputerInfo
14 |
15 | // Get user info from functions in class
16 | @EnvironmentObject var userinfo: UserInfo
17 |
18 | // Get preferences or default values
19 | @StateObject var preferences = Preferences()
20 |
21 | // Make UserDefaults easy to use
22 | let defaults = UserDefaults.standard
23 |
24 | // Dark Mode detection
25 | @Environment(\.colorScheme) var colorScheme
26 |
27 | var body: some View {
28 |
29 | // MARK: - Horizontal stack with Title and Logo
30 | HStack(spacing: 10) {
31 |
32 | // Supports for markdown through a variable:
33 | Text(.init(preferences.title.replaceLocalVariables(computerInfo: computerinfo, userInfo: userinfo)))
34 | .font(.system(size: 20, design: .rounded))
35 | .fontWeight(.medium)
36 | .fixedSize()
37 |
38 | Spacer()
39 |
40 | // Logo shown in the top right corner
41 | if colorScheme == .light && defaults.string(forKey: "Logo") != nil {
42 | LogoView(logo: defaults.string(forKey: "Logo")!)
43 | // Show different logo in Dark Mode when LogoDarkMode is also set
44 | } else if colorScheme == .dark && defaults.string(forKey: "Logo") != nil {
45 | if defaults.string(forKey: "LogoDarkMode") != nil {
46 | LogoView(logo: defaults.string(forKey: "LogoDarkMode")!)
47 | } else if defaults.string(forKey: "Logo") != nil && defaults.string(forKey: "LogoDarkMode") == nil {
48 | LogoView(logo: defaults.string(forKey: "Logo")!)
49 | } else {
50 | LogoView(logo: "default")
51 | }
52 | // Show default logo in all other cases
53 | } else {
54 | LogoView(logo: "default")
55 | }
56 |
57 | }
58 | .foregroundColor(Color.primary)
59 | .padding(.leading, 16.0)
60 | .padding(.trailing, 10.0)
61 | .padding(.top, 10.0)
62 | .unredacted()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Support/Views/LogoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogoView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 16/11/2022.
6 | // Inspired by Bart Reardon to support SF Symbol color options
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct LogoView: View {
12 |
13 | var logo: String
14 |
15 | // Get string between "SF=" and optionally ","
16 | var symbol: String {
17 | if logo.components(separatedBy: ",").indices.contains(0) {
18 | let symbolString = logo.components(separatedBy: ",")[0]
19 | return symbolString.replacingOccurrences(of: "SF=", with: "")
20 | } else {
21 | return logo.replacingOccurrences(of: "SF=", with: "")
22 | }
23 | }
24 |
25 | // Get string for SF Symbol color option
26 | var symbolColor: String {
27 | if logo.components(separatedBy: ",color=").indices.contains(1) {
28 | return logo.components(separatedBy: ",color=")[1]
29 | } else {
30 | return ""
31 | }
32 | }
33 |
34 | var body: some View {
35 |
36 | // When "http" prefix is detected, try to fetch image from URL
37 | if logo.hasPrefix("http") {
38 |
39 | AsyncImage(url: URL(string: logo)) { image in
40 | image.resizable()
41 | .scaledToFit()
42 | .frame(height: 48)
43 |
44 | } placeholder: {
45 | Image("DefaultLogo")
46 | .resizable()
47 | .scaledToFit()
48 | .cornerRadius(6)
49 | .redacted(reason: .placeholder)
50 | .overlay(
51 | ProgressView()
52 | )
53 | .frame(width: 48, height: 48)
54 | }
55 | // When "SF=" prefix is detected, try to show SF Symbol with optional color options
56 | } else if logo.hasPrefix("SF=") {
57 |
58 | switch symbolColor {
59 | case "auto":
60 | Image(systemName: symbol)
61 | .resizable()
62 | .foregroundColor(.accentColor)
63 | .scaledToFit()
64 | .frame(height: 48)
65 | case "multicolor":
66 | Image(systemName: symbol)
67 | .symbolRenderingMode(.multicolor)
68 | .resizable()
69 | .scaledToFit()
70 | .frame(height: 48)
71 | case "hierarchical":
72 | Image(systemName: symbol)
73 | .symbolRenderingMode(.hierarchical)
74 | .resizable()
75 | .scaledToFit()
76 | .frame(height: 48)
77 | case _ where symbolColor.hasPrefix("#"):
78 | Image(systemName: symbol)
79 | .resizable()
80 | .foregroundColor(Color(NSColor(hex: "\(symbolColor)") ?? NSColor.controlAccentColor))
81 | .scaledToFit()
82 | .frame(height: 48)
83 | default:
84 | Image(systemName: symbol)
85 | .resizable()
86 | .scaledToFit()
87 | .frame(height: 48)
88 | }
89 | // Show default logo
90 | } else if logo == "default" {
91 | Image("DefaultLogo")
92 | .resizable()
93 | .scaledToFit()
94 | .frame(height: 48)
95 | // In all other cases the file is expected to be local but fallback to default logo when not present
96 | } else {
97 | Image(nsImage: (NSImage(contentsOfFile: logo) ?? NSImage(named: "DefaultLogo"))!)
98 | .resizable()
99 | .scaledToFit()
100 | .frame(height: 48)
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/Support/Views/NotificationBadgeTextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationBadgeTextView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 12/04/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NotificationBadgeTextView: View {
11 |
12 | var badgeCounter: String
13 |
14 | var body: some View {
15 |
16 | VStack {
17 |
18 | HStack {
19 |
20 | Spacer()
21 |
22 | Circle()
23 | .foregroundColor(.orange)
24 | .overlay(
25 | Text("\(badgeCounter)")
26 | .foregroundColor(.white)
27 | .font(Font.system(size: 12))
28 | )
29 | .frame(width: 18, height: 18)
30 | .padding(4)
31 | }
32 | Spacer()
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Support/Views/NotificationBadgeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationBadgeView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 30/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NotificationBadgeView: View {
11 |
12 | var badgeCounter: Int
13 |
14 | var body: some View {
15 |
16 | VStack {
17 |
18 | HStack {
19 |
20 | Spacer()
21 |
22 | Circle()
23 | .foregroundColor(.red)
24 | .overlay(
25 | Text("\(badgeCounter)")
26 | .foregroundColor(.white)
27 | .font(Font.system(size: 12))
28 | )
29 | .frame(width: 18, height: 18)
30 | .padding(4)
31 | }
32 | Spacer()
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Support/Views/PopoverAlertView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PopoverAlertView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 22/08/2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @available(macOS 12.0, *)
11 | struct PopoverAlertView: View {
12 |
13 | // Get preferences or default values
14 | @StateObject var preferences = Preferences()
15 |
16 | // Make UserDefaults easy to use
17 | let defaults = UserDefaults.standard
18 |
19 | // Dark Mode detection
20 | @Environment(\.colorScheme) var colorScheme
21 |
22 | @Binding var uptimeAlert: Bool
23 |
24 | var title: String
25 | var message: String
26 |
27 | var body: some View {
28 |
29 | VStack(spacing: 8) {
30 |
31 | // Show custom Notification Icon when specified
32 | if defaults.string(forKey: "NotificationIcon") != nil {
33 | LogoView(logo: defaults.string(forKey: "NotificationIcon")!)
34 | } else {
35 | LogoView(logo: "default")
36 | }
37 |
38 | Text(title)
39 | // Set frame to 250 to allow multiline text
40 | .frame(width: 250)
41 | .fixedSize()
42 | .font(.system(.headline, design: .rounded))
43 |
44 | Text(message)
45 | // Set frame to 250 to allow multiline text
46 | .frame(width: 250)
47 | .fixedSize()
48 | .font(.system(.body, design: .rounded))
49 |
50 | Button( action: {
51 | self.uptimeAlert.toggle()
52 | }) {
53 | Text(NSLocalizedString("CLOSE", comment: ""))
54 | .font(.system(.body, design: .rounded))
55 | .fontWeight(.regular)
56 | .padding(.vertical, 4)
57 | .padding(.horizontal)
58 | .background(colorScheme == .dark ? .white.opacity(0.2) : .black.opacity(0.1))
59 | .clipShape(Capsule())
60 | }
61 | .buttonStyle(.plain)
62 | .padding(.top)
63 | }
64 | .padding()
65 | .unredacted()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Support/Views/QuitButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuitButton.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 04/01/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct QuitButton: View {
11 |
12 | @State var quitPressed = false
13 |
14 | var body: some View {
15 |
16 | VStack {
17 | HStack {
18 | Image(systemName: "xmark.circle.fill")
19 | .padding(4)
20 | .onTapGesture {
21 | self.quitPressed = true
22 | }
23 | .help(NSLocalizedString("Quit", comment: ""))
24 | .alert(isPresented: $quitPressed) {
25 | Alert(title: Text(NSLocalizedString("Are you sure you want to quit?", comment: "")), message: Text(""), primaryButton: .default(Text(NSLocalizedString("Quit", comment: ""))) {
26 | NSApplication.shared.terminate(AppDelegate.self)
27 | }, secondaryButton: .cancel())
28 | }
29 |
30 | Spacer()
31 | }
32 | Spacer()
33 | }
34 | .unredacted()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Support/Views/StatusItemBadgeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusItemBadgeView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 21/09/2022.
6 | //
7 |
8 | import Cocoa
9 | import Foundation
10 |
11 | // Notification badge drawing view
12 | // https://stackoverflow.com/questions/68671831/swiftui-macos-menubar-icon-with-badge
13 | class StatusItemBadgeView: NSView {
14 |
15 | var color: NSColor
16 |
17 | init(frame frameRect: NSRect, color : NSColor) {
18 | self.color = color
19 | super.init(frame: frameRect)
20 | translatesAutoresizingMaskIntoConstraints = false
21 | wantsLayer = true
22 | }
23 |
24 | required init?(coder: NSCoder) {
25 | self.color = NSColor()
26 | super.init(coder: coder)
27 | }
28 |
29 | override func draw(_ dirtyRect: NSRect) {
30 | let fillColor = color
31 | let path = NSBezierPath(ovalIn: NSRect(x: 0, y: 0, width: 8, height: 8))
32 | fillColor.set()
33 | path.fill()
34 | }
35 |
36 | override func layout() {
37 | super.layout()
38 | layer?.masksToBounds = true
39 | layer?.backgroundColor = color.cgColor
40 |
41 | layer?.borderColor = NSColor.windowBackgroundColor.cgColor
42 | layer?.borderWidth = 0.5
43 | layer?.cornerRadius = frame.size.height / 2.0
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Support/Views/ViewModifiers/DarkModeBorder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DarkModeBorder.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 05/05/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Apply gray and black border in Dark Mode to better view the buttons like Control Center
11 | struct DarkModeBorder: ViewModifier {
12 |
13 | // Dark Mode detection
14 | @Environment(\.colorScheme) var colorScheme
15 |
16 | func body(content: Content) -> some View {
17 |
18 | if colorScheme == .dark {
19 | content
20 | .overlay(
21 | RoundedRectangle(cornerRadius: 10)
22 | .strokeBorder(Color.white, lineWidth: 0.5, antialiased: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
23 | .opacity(0.15))
24 | .shadow(color: .black, radius: 0.5, x: 0, y: 0)
25 | } else {
26 | content
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Support/Views/WelcomeScreen/FeatureView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FeatureView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 23/06/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FeatureView: View {
11 |
12 | var image: String
13 | var title: String
14 | var subtitle: String
15 | var color: Color
16 |
17 | var body: some View {
18 |
19 | HStack(spacing: 24) {
20 | Image(systemName: image)
21 | .resizable()
22 | .aspectRatio(contentMode: .fit)
23 | .frame(width: 35, height: 35)
24 | .foregroundColor(color)
25 |
26 | VStack(alignment: .leading, spacing: 2) {
27 | Text(title)
28 | .font(.system(.body, design: .rounded)).fontWeight(.medium)
29 | // .font(.system(.headline, design: .rounded))
30 | // .fontWeight(.bold)
31 | Text(subtitle)
32 | .font(.system(.subheadline, design: .rounded))
33 | // .font(.system(.subheadline, design: .rounded))
34 | // .fontWeight(.semibold)
35 | .foregroundColor(.secondary)
36 | }
37 |
38 | Spacer()
39 |
40 | }
41 | .frame(width: 250, height: 60)
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/Support/Views/WelcomeScreen/WelcomeScreenView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WelcomeView.swift
3 | // Support
4 | //
5 | // Created by Jordy Witteman on 23/06/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct WelcomeView: View {
11 |
12 | @EnvironmentObject var computerinfo: ComputerInfo
13 | @EnvironmentObject var userinfo: UserInfo
14 |
15 | // Get preferences or default values
16 | @StateObject var preferences = Preferences()
17 |
18 | // Make UserDefaults easy to use
19 | let defaults = UserDefaults.standard
20 |
21 | // Dark Mode detection
22 | @Environment(\.colorScheme) var colorScheme
23 |
24 | // Set the custom color for all symbols depending on Light or Dark Mode.
25 | var customColor: String {
26 | if colorScheme == .light && defaults.string(forKey: "CustomColor") != nil {
27 | return preferences.customColor
28 | } else if colorScheme == .dark && defaults.string(forKey: "CustomColorDarkMode") != nil {
29 | return preferences.customColorDarkMode
30 | } else {
31 | return preferences.customColor
32 | }
33 | }
34 |
35 | @State var hoverButton = false
36 |
37 | var body: some View {
38 |
39 | VStack(alignment: .leading) {
40 |
41 | FeatureView(image: "stethoscope", title: NSLocalizedString("Mac diagnosis", comment: ""), subtitle: NSLocalizedString("MAC_DIAGNOSIS_TEXT", comment: ""), color: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor))
42 |
43 | FeatureView(image: "briefcase", title: NSLocalizedString("Easy access", comment: ""), subtitle: NSLocalizedString("EASY_ACCESS_TEXT", comment: ""), color: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor))
44 |
45 | FeatureView(image: "lifepreserver", title: NSLocalizedString("Get in touch", comment: ""), subtitle: NSLocalizedString("GET_IN_TOUCH_TEXT", comment: ""), color: Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor))
46 |
47 | }
48 |
49 | HStack {
50 | Spacer()
51 | Text(NSLocalizedString("Continue", comment: ""))
52 | .font(.system(.body, design: .rounded))
53 | .fontWeight(.bold)
54 | .foregroundColor(.white)
55 | Spacer()
56 | }
57 | .frame(width: 200, height: 35)
58 | .background(Color(NSColor(hex: "\(customColor)") ?? NSColor.controlAccentColor))
59 | .opacity(hoverButton ? 0.5 : 1.0)
60 | .cornerRadius(10)
61 | .onTapGesture {
62 | preferences.hasSeenWelcomeScreen.toggle()
63 | }
64 | .onHover {_ in
65 | withAnimation(.easeInOut) {
66 | hoverButton.toggle()
67 | }
68 | }
69 | .padding(.top)
70 | }
71 | }
72 |
73 | struct WelcomeView_Previews: PreviewProvider {
74 | static var previews: some View {
75 | WelcomeView()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Support/de.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Support
4 |
5 | Created by Jordy Witteman on 03/09/2020.
6 |
7 | */
8 |
9 | "Computer Name" = "Computername";
10 | "Model" = "Modell";
11 | "Last Reboot" = "Letzter Neustart";
12 | "RESTART_REGULARLY" = "Starten Sie Ihren Mac regelmäßig neu";
13 | "ADMIN_RECOMMENDS_RESTARTING_EVERY" = "Dein Administrator empfiehlt dir einen Neustart alle";
14 | "ADMIN_RECOMMENDS_RESTARTING_EVERY_DAY" = "Dein Administrator empfiehlt Dir, täglich einen Neustart durchzuführen.";
15 | "ago" = "vergangen";
16 | "minute" = "Minute";
17 | "minutes" = "Minuten";
18 | "hour" = "Stunde";
19 | "hours" = "Stunden";
20 | "day" = "Tag";
21 | "days" = "Tage";
22 | "Used" = "belegt";
23 | "Available" = "verfügbar";
24 | "About Support" = "Über Support";
25 | "Help and Documentation" = "Hilfe and Dokumentation";
26 | "Quit Support" = "Support beenden";
27 | "Quit" = "Schließen";
28 | "Are you sure you want to quit?" = "Sicher, dass du beenden möchtest?";
29 | "An error occurred" = "Ein Fehler ist aufgetreten";
30 | "Please contact IT support" = "Bitte kontaktiere dein IT Support";
31 | "Not Connected" = "Nicht verbunden";
32 | "IP Address" = "IP Adresse";
33 | "No IP Address" = "Keine IP Adresse";
34 | "Password" = "Passwort";
35 | "Expires Today" = "Läuft heute ab";
36 | "Never Expires" = "Läuft niemals ab";
37 | "Expires in " = "Läuft ab in ";
38 | "Expired" = "Abgelaufen";
39 | "Change Now" = "Jetzt ändern";
40 | "Sign In Here" = "Hier anmelden";
41 | " days" = " Tage";
42 | " day" = " Tag";
43 | "Mac diagnosis" = "Mac Diagnose";
44 | "MAC_DIAGNOSIS_TEXT" = "Erhalte einen Überblick über dein aktuellen Zustand deines Macs";
45 | "Easy access" = "Schneller Aufruf";
46 | "EASY_ACCESS_TEXT" = "Starte Organisationsressourcen wie Apps und Websites mit einem Klick";
47 | "Get in touch" = "Trete in Kontakt";
48 | "GET_IN_TOUCH_TEXT" = "Erstelle ganz einfach ein Ticket oder eine Anfrage";
49 | "Continue" = "Fortfahren";
50 | "OPEN_JAMF_CONNECT_MANUALLY" = "Bitte öffne Jamf Connect";
51 | "OPEN_JAMF_CONNECT_MANUALLY_TEXT" = "Öffne Jamf Connect, um dein Passwort zu ändern";
52 | "NETWORK_UNAVAILABLE" = "Netzwerk nicht verfügbar";
53 | "NETWORK_UNAVAILABLE_TEXT" = "Das Netzwerk deiner Organisation ist nicht verfügbar. Bitte prüfe die Verbindung und versuche es erneut.";
54 | "CLOSE" = "Schließe";
55 | "UPDATES_AVAILABLE" = "Updates verfügbar";
56 | "NO_UPDATES_AVAILABLE" = "Keine Updates verfügbar";
57 | "UPDATE_NOW" = "Jetzt aktualisieren";
58 | "SYSTEM_SETTINGS" = "Systemeinstellungen";
59 | "SYSTEM_PREFERENCES" = "Systemeinstellungen";
60 | "YOUR_MAC_IS_UP_TO_DATE" = "Ihr Mac ist auf dem neuesten Stand";
61 | "UPDATE" = "Aktualisieren";
62 | "APP_CATALOG" = "App Catalog";
63 | "APP_CATALOG_NOT_CONFIGURED" = "App Catalog ist nicht korrekt eingerichtet";
64 | "DOCUMENTATION" = "Documentation";
65 | "ALL_APPS_UP_TO_DATE" = "Alle Anwendungen sind auf dem neuesten Stand";
66 | "UPDATE_ALL" = "Alle aktualisieren";
67 | "APP_UPDATES" = "App-Aktualisierungen";
68 | "MACOS_UPDATES" = "macOS-Updates";
69 | "AUTOMATIC_INSTALLATION" = "Automatische Installation";
70 | "MORE_INFO" = "Mehr Infos";
71 | "RESTART_NOW" = "Bitte jetzt neu starten";
72 | "RESTART" = "Neustart";
73 | "APPS_WILL_BE_UPDATED_AUTOMATICALLY_DESCRIPTION" = "Die Anwendungen werden automatisch aktualisiert";
74 | "ENFORCED_ON" = "Durchgesetzt am";
75 | "DETAILS" = "Einzelheiten";
76 | "APPS" = "Apps";
77 | "PRIVILEGED_HELPER_TOOL_NOT_INSTALLED" = "Privileged Helper Tool ist nicht installiert";
78 | "UP_TO_DATE" = "Auf dem neuesten Stand";
79 |
--------------------------------------------------------------------------------
/src/Support/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Support
4 |
5 | Created by Jordy Witteman on 03/09/2020.
6 |
7 | */
8 |
9 | "Computer Name" = "Computer Name";
10 | "Model" = "Model";
11 | "Last Reboot" = "Last Reboot";
12 | "RESTART_REGULARLY" = "Restart your Mac regularly";
13 | "ADMIN_RECOMMENDS_RESTARTING_EVERY" = "Your administrator recommends restarting your Mac every";
14 | "ADMIN_RECOMMENDS_RESTARTING_EVERY_DAY" = "Your administrator recommends restarting your Mac daily.";
15 | "ago" = "ago";
16 | "minute" = "minute";
17 | "minutes" = "minutes";
18 | "hour" = "hour";
19 | "hours" = "hours";
20 | "day" = "day";
21 | "days" = "days";
22 | "Used" = "Used";
23 | "Available" = "Available";
24 | "About Support" = "About Support";
25 | "Help and Documentation" = "Help and Documentation";
26 | "Quit Support" = "Quit Support";
27 | "Quit" = "Quit";
28 | "Are you sure you want to quit?" = "Are you sure you want to quit?";
29 | "An error occurred" = "An error occurred";
30 | "Please contact IT support" = "Please contact IT support";
31 | "Not Connected" = "Not connected";
32 | "IP Address" = "IP Address";
33 | "No IP Address" = "No IP Address";
34 | "Password" = "Password";
35 | "Expires Today" = "Expires Today";
36 | "Never Expires" = "Never Expires";
37 | "Expires in " = "Expires in ";
38 | "Expired" = "Expired";
39 | "Change Now" = "Change Now";
40 | "Sign In Here" = "Sign In Here";
41 | " days" = " days";
42 | " day" = " day";
43 | "Mac diagnosis" = "Mac diagnosis";
44 | "MAC_DIAGNOSIS_TEXT" = "An overview of your Mac and helps you keeping it healthy";
45 | "Easy access" = "Easy access";
46 | "EASY_ACCESS_TEXT" = "Get access to organization resources like apps and sites";
47 | "Get in touch" = "Get in touch";
48 | "GET_IN_TOUCH_TEXT" = "Easily create a ticket or submit a request";
49 | "Continue" = "Continue";
50 | "OPEN_JAMF_CONNECT_MANUALLY" = "Please open Jamf Connect";
51 | "OPEN_JAMF_CONNECT_MANUALLY_TEXT" = "Open Jamf Connect from the menu bar to change your password";
52 | "NETWORK_UNAVAILABLE" = "Network Unavailable";
53 | "NETWORK_UNAVAILABLE_TEXT" = "Your organisation's network isn't available. Please check your connection and try again.";
54 | "CLOSE" = "Close";
55 | "UPDATES_AVAILABLE" = "Updates Available";
56 | "NO_UPDATES_AVAILABLE" = "No updates available";
57 | "UPDATE_NOW" = "Update Now";
58 | "SYSTEM_SETTINGS" = "System Settings";
59 | "SYSTEM_PREFERENCES" = "System Preferences";
60 | "YOUR_MAC_IS_UP_TO_DATE" = "Your Mac is up to date";
61 | "UPDATE" = "Update";
62 | "APP_CATALOG" = "App Catalog";
63 | "ALL_APPS_UP_TO_DATE" = "All apps are up to date";
64 | "UPDATE_ALL" = "Update All";
65 | "APP_UPDATES" = "App Updates";
66 | "APP_CATALOG_NOT_CONFIGURED" = "App Catalog is not setup correctly";
67 | "DOCUMENTATION" = "Documentation";
68 | "MACOS_UPDATES" = "macOS Updates";
69 | "ENFORCED_ON" = "Enforced on";
70 | "DETAILS" = "Details";
71 | "RESTART_NOW" = "Please restart now";
72 | "RESTART" = "Restart";
73 | "APPS_WILL_BE_UPDATED_AUTOMATICALLY_DESCRIPTION" = "Apps will be automatically updated";
74 | "APPS" = "Apps";
75 | "PRIVILEGED_HELPER_TOOL_NOT_INSTALLED" = "Privileged Helper Tool is not installed";
76 | "UP_TO_DATE" = "Up to date";
77 |
--------------------------------------------------------------------------------
/src/Support/es.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings Spanish
3 | Support
4 |
5 | Created by Jovicom on 05/03/2023.
6 |
7 | */
8 |
9 | "Computer Name" = "Nombre de equipo";
10 | "Model" = "Modelo";
11 | "Last Reboot" = "Dias sin reiniciar";
12 | "RESTART_REGULARLY" = "Reinicia tu Mac con regularidad";
13 | "ADMIN_RECOMMENDS_RESTARTING_EVERY" = "El administrador recomienda reiniciar el Mac cada";
14 | "ADMIN_RECOMMENDS_RESTARTING_EVERY_DAY" = "El administrador recomienda reiniciar el Mac a diario.";
15 | "ago" = "hace";
16 | "minute" = "minuto";
17 | "minutes" = "minutos";
18 | "hour" = "hora";
19 | "hours" = "horas";
20 | "day" = "dia";
21 | "days" = "dias";
22 | "Used" = "Usado";
23 | "Available" = "Disponible";
24 | "About Support" = "Acerca de Support";
25 | "Help and Documentation" = "Ayuda y Documentación";
26 | "Quit Support" = "Salir Support";
27 | "Quit" = "Salir";
28 | "Are you sure you want to quit?" = "¿Estas seguro de salir?";
29 | "An error occurred" = "Ha ocurrido un error";
30 | "Please contact IT support" = "Por favor, contacta con soporte IT";
31 | "Not Connected" = "Sin conectar";
32 | "IP Address" = "Dirección IP";
33 | "No IP Address" = "Sin dirección IP";
34 | "Password" = "Contraseña";
35 | "Expires Today" = "Caduca hoy";
36 | "Never Expires" = "Nunca caduca";
37 | "Expires in " = "Caduca el ";
38 | "Expired" = "Caducada";
39 | "Change Now" = "Cambiar ahora";
40 | "Sign In Here" = "Loguéate aquí";
41 | " days" = " dias";
42 | " day" = " dia";
43 | "Mac diagnosis" = "Diagnosis del Mac";
44 | "MAC_DIAGNOSIS_TEXT" = "Un vistazo rápido de tu Mac para ayudarte a mantenerlo óptimo";
45 | "Easy access" = "Fácil acceso";
46 | "EASY_ACCESS_TEXT" = "Accede a los recursos de tu organización como apps y sitios web";
47 | "Get in touch" = "Ponerse en contacto";
48 | "GET_IN_TOUCH_TEXT" = "Crea facilmente un ticket o sube una consulta";
49 | "Continue" = "Continuar";
50 | "OPEN_JAMF_CONNECT_MANUALLY" = "Por favor, abre Jamf Connect";
51 | "OPEN_JAMF_CONNECT_MANUALLY_TEXT" = "Abre Jamf Connect desde la barra de menus para cambiar tu contraseña";
52 | "NETWORK_UNAVAILABLE" = "Red no disponible";
53 | "NETWORK_UNAVAILABLE_TEXT" = "La red de tu organización no está disponible. Por favor, chequea tu conexión e inténtalo de nuevo.";
54 | "CLOSE" = "Cerrar";
55 | "UPDATES_AVAILABLE" = "Actualizaciones disponibles";
56 | "NO_UPDATES_AVAILABLE" = "No hay actualizaciones disponibles";
57 | "UPDATE_NOW" = "Actualizar ahora";
58 | "SYSTEM_SETTINGS" = "Ajustes del Sistema";
59 | "SYSTEM_PREFERENCES" = "Preferencias del Sistema";
60 | "YOUR_MAC_IS_UP_TO_DATE" = "Tu Mac está actualizado";
61 | "UPDATE" = "Actualizar";
62 | "APP_CATALOG" = "App Catalog";
63 | "APP_CATALOG_NOT_CONFIGURED" = "App Catalog no está configurado correctamente";
64 | "DOCUMENTATION" = "Documentation";
65 | "ALL_APPS_UP_TO_DATE" = "Todas las aplicaciones están actualizadas";
66 | "UPDATE_ALL" = "Actualizar todo";
67 | "APP_UPDATES" = "Actualizaciones de aplicaciónes";
68 | "MACOS_UPDATES" = "Actualizaciones de macOS";
69 | "AUTOMATIC_INSTALLATION" = "Instalación automática";
70 | "MORE_INFO" = "Más información";
71 | "RESTART_NOW" = "Por favor, reinicie ahora";
72 | "RESTART" = "Reiniciar";
73 | "APPS_WILL_BE_UPDATED_AUTOMATICALLY_DESCRIPTION" = "Las aplicaciones se actualizarán automáticamente";
74 | "ENFORCED_ON" = "Aplicada en";
75 | "DETAILS" = "Detalles";
76 | "APPS" = "Apps";
77 | "PRIVILEGED_HELPER_TOOL_NOT_INSTALLED" = "Privileged Helper Tool no está instalado";
78 | "UP_TO_DATE" = "Actualizado";
79 |
--------------------------------------------------------------------------------
/src/Support/fr.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Support
4 |
5 | Translated by Guillaume Gète on 19/08/2021.
6 |
7 | */
8 |
9 | "Computer Name" = "Nom de ce Mac";
10 | "Model" = "Modèle";
11 | "Last Reboot" = "Dernier redémarrage";
12 | "RESTART_REGULARLY" = "Pensez à redémarrer votre Mac";
13 | "ADMIN_RECOMMENDS_RESTARTING_EVERY" = "Votre administrateur préféré recommande de redémarrer votre Mac tous les";
14 | "ADMIN_RECOMMENDS_RESTARTING_EVERY_DAY" = "Votre administrateur vous recommande de redémarrer votre Mac tous les jours.";
15 | "ago" = "plus tôt";
16 | "minute" = "minute";
17 | "minutes" = "minutes";
18 | "hour" = "heure";
19 | "hours" = "heures";
20 | "day" = "jour";
21 | "days" = "jours";
22 | "Used" = "Utilisé";
23 | "Available" = "Disponible";
24 | "About Support" = "À propos de Support";
25 | "Help and Documentation" = "Aide et documentation";
26 | "Quit Support" = "Quitter Support";
27 | "Quit" = "Quitter";
28 | "Are you sure you want to quit?" = "Voulez-vous vraiment quitter?";
29 | "An error occurred" = "Une erreur est survenue";
30 | "Please contact IT support" = "Contactez le support technique";
31 | "Not Connected" = "Non connecté";
32 | "IP Address" = "Adresse IP";
33 | "No IP Address" = "Pas d'adresse IP";
34 | "Password" = "Mot de passe";
35 | "Expires Today" = "Expire aujourd'hui'";
36 | "Never Expires" = "N'expire jamais'";
37 | "Expires in " = "Expire dans ";
38 | "Expired" = "Expiré";
39 | "Change Now" = "Changer maintenant";
40 | "Sign In Here" = "Connectez-vous ici";
41 | " days" = " jours";
42 | " day" = " jour";
43 | "Mac diagnosis" = "Diagnostic du Mac";
44 | "MAC_DIAGNOSIS_TEXT" = "Un aperçu de votre Mac, et un état de sa santé";
45 | "Easy access" = "Accès simplifié";
46 | "EASY_ACCESS_TEXT" = "Accédez aux ressource de votre organisation : apps et sites";
47 | "Get in touch" = "Prendre contact";
48 | "GET_IN_TOUCH_TEXT" = "Créez simplement un ticket ou soumettez une demande";
49 | "Continue" = "Continuer";
50 | "OPEN_JAMF_CONNECT_MANUALLY" = "Veuillez ouvrir Jamf Connect";
51 | "OPEN_JAMF_CONNECT_MANUALLY_TEXT" = "Ouvrez Jamf Connect depuis la barre des menus pour changer votre mot de passe";
52 | "NETWORK_UNAVAILABLE" = "Réseau indisponible";
53 | "NETWORK_UNAVAILABLE_TEXT" = "Le réseau de votre organisation n'est pas disponible. Veuillez vérifier votre connexion et tentez une nouvelle fois.";
54 | "CLOSE" = "Ferme";
55 | "UPDATES_AVAILABLE" = "Mises à jour disponibles";
56 | "NO_UPDATES_AVAILABLE" = "Aucune mise à jour disponible";
57 | "UPDATE_NOW" = "Mettre à jour maintenant";
58 | "SYSTEM_SETTINGS" = "Réglages Système";
59 | "SYSTEM_PREFERENCES" = "Préférences Système";
60 | "YOUR_MAC_IS_UP_TO_DATE" = "Votre Mac est à jour";
61 | "UPDATE" = "Mettre à jour";
62 | "APP_CATALOG" = "App Catalog";
63 | "APP_CATALOG_NOT_CONFIGURED" = "Le App Catalog n'est pas configuré correctement";
64 | "DOCUMENTATION" = "Documentation";
65 | "ALL_APPS_UP_TO_DATE" = "Toutes les applications sont à jour";
66 | "UPDATE_ALL" = "Tout mettre à jour";
67 | "APP_UPDATES" = "Mises à jour de l'application";
68 | "MACOS_UPDATES" = "Mises à jour de macOS";
69 | "AUTOMATIC_INSTALLATION" = "Installation automatique";
70 | "MORE_INFO" = "Plus d'informations";
71 | "RESTART_NOW" = "Veuillez redémarrer maintenant";
72 | "RESTART" = "Redémarrage";
73 | "APPS_WILL_BE_UPDATED_AUTOMATICALLY_DESCRIPTION" = "Les applications seront automatiquement mises à jour";
74 | "ENFORCED_ON" = "Appliqué le";
75 | "DETAILS" = "Détails";
76 | "APPS" = "Apps";
77 | "PRIVILEGED_HELPER_TOOL_NOT_INSTALLED" = "Le Privileged Helper Tool n'est pas installé";
78 | "UP_TO_DATE" = "A jour";
79 |
--------------------------------------------------------------------------------
/src/Support/nl-NL.lproj/Main.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */
3 | "computerNameText" = "Computer Naam";
4 |
--------------------------------------------------------------------------------
/src/Support/nl.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Support
4 |
5 | Created by Jordy Witteman on 03/09/2020.
6 |
7 | */
8 |
9 | "Computer Name" = "Computernaam";
10 | "Model" = "Model";
11 | "Last Reboot" = "Laatste herstart";
12 | "RESTART_REGULARLY" = "Herstart je Mac regelmatig";
13 | "ADMIN_RECOMMENDS_RESTARTING_EVERY" = "Je beheerder raadt aan je Mac te herstarten tenminste iedere";
14 | "ADMIN_RECOMMENDS_RESTARTING_EVERY_DAY" = "Je beheerder raadt aan je Mac dagelijks te herstarten.";
15 | "ago" = "geleden";
16 | "minute" = "minuut";
17 | "minutes" = "minuten";
18 | "hour" = "uur";
19 | "hours" = "uur";
20 | "day" = "dag";
21 | "days" = "dagen";
22 | "Used" = "vol";
23 | "Available" = "beschikbaar";
24 | "About Support" = "Over Support";
25 | "Help and Documentation" = "Help en Documentatie";
26 | "Quit Support" = "Stop Support";
27 | "Quit" = "Stop";
28 | "Are you sure you want to quit?" = "Weet je zeker dat je wilt afsluiten?";
29 | "An error occurred" = "Er was een probleem";
30 | "Please contact IT support" = "Neem contact op met de servicedesk";
31 | "Not Connected" = "Niet verbonden";
32 | "IP Address" = "IP adres";
33 | "No IP Address" = "Geen IP adres";
34 | "Password" = "Wachtwoord";
35 | "Expires Today" = "Verloopt vandaag";
36 | "Never Expires" = "Verloopt nooit";
37 | "Expires in " = "Verloopt over ";
38 | "Expired" = "Verlopen";
39 | "Change Now" = "Wijzig nu";
40 | "Sign In Here" = "Log in";
41 | " days" = " dagen";
42 | " day" = " dag";
43 | "Mac diagnosis" = "Mac diagnose";
44 | "MAC_DIAGNOSIS_TEXT" = "Een overzicht van je Mac en helpt je deze gezond te houden";
45 | "Easy access" = "Snel toegang";
46 | "EASY_ACCESS_TEXT" = "Krijg toegang tot apps en sites van de organisatie";
47 | "Get in touch" = "Neem contact op";
48 | "GET_IN_TOUCH_TEXT" = "Maak snel een ticket aan of dien een verzoek in";
49 | "Continue" = "Ga door";
50 | "OPEN_JAMF_CONNECT_MANUALLY" = "Open Jamf Connect";
51 | "OPEN_JAMF_CONNECT_MANUALLY_TEXT" = "Open Jamf Connect in de menubalk om je wachtwoord te wijzigen";
52 | "NETWORK_UNAVAILABLE" = "Netwerk niet beschikbaar";
53 | "NETWORK_UNAVAILABLE_TEXT" = "Het netwerk van je organisatie is niet beschikbaar. Controleer je verbinding en probeer het opnieuw.";
54 | "CLOSE" = "Sluit";
55 | "UPDATES_AVAILABLE" = "Updates beschikbaar";
56 | "NO_UPDATES_AVAILABLE" = "Geen updates beschikbaar";
57 | "UPDATE_NOW" = "Nu bijwerken";
58 | "SYSTEM_SETTINGS" = "Systeeminstellingen";
59 | "SYSTEM_PREFERENCES" = "Systeemvoorkeuren";
60 | "YOUR_MAC_IS_UP_TO_DATE" = "Je Mac is up-to-date";
61 | "UPDATE" = "Werk bij";
62 | "APP_CATALOG" = "App Catalog";
63 | "ALL_APPS_UP_TO_DATE" = "Alle apps zijn bijgewerkt";
64 | "UPDATE_ALL" = "Alles bijwerken";
65 | "APP_UPDATES" = "App updates";
66 | "APP_CATALOG_NOT_CONFIGURED" = "App Catalog is niet correct ingesteld";
67 | "DOCUMENTATION" = "Documentation";
68 | "MACOS_UPDATES" = "macOS Updates";
69 | "ENFORCED_ON" = "Afgedwongen op";
70 | "DETAILS" = "Details";
71 | "RESTART_NOW" = "Herstart je Mac nu";
72 | "RESTART" = "Herstart";
73 | "APPS_WILL_BE_UPDATED_AUTOMATICALLY_DESCRIPTION" = "Apps worden automatisch bijgewerkt";
74 | "AUTOMATIC_INSTALLATION" = "Automatische installatie";
75 | "APPS" = "Apps";
76 | "PRIVILEGED_HELPER_TOOL_NOT_INSTALLED" = "Privileged Helper Tool is niet geïnstalleerd";
77 | "UP_TO_DATE" = "Bijgewerkt";
78 |
--------------------------------------------------------------------------------
/src/Support/nl.root3.support.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BundleProgram
6 | Contents/MacOS/Support
7 | KeepAlive
8 |
9 | Label
10 | nl.root3.support
11 | ProcessType
12 | Interactive
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/SupportHelper/AuditTokenHack.h:
--------------------------------------------------------------------------------
1 | //
2 | // AuditTokenHack.h
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | #ifndef AuditTokenHack_h
9 | #define AuditTokenHack_h
10 |
11 |
12 | #endif /* AuditTokenHack_h */
13 |
14 | #import
15 |
16 | // Hack to get the private auditToken property
17 | @interface NSXPCConnection(PrivateAuditToken)
18 |
19 | @property (nonatomic, readonly) audit_token_t auditToken;
20 |
21 | @end
22 |
23 | // Interface for AuditTokenHack
24 | @interface AuditTokenHack : NSObject
25 |
26 | +(NSData *)getAuditTokenDataFromNSXPCConnection:(NSXPCConnection *)connection;
27 |
28 | @end
29 |
--------------------------------------------------------------------------------
/src/SupportHelper/AuditTokenHack.m:
--------------------------------------------------------------------------------
1 | //
2 | // AuditTokenHack.m
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | #import
9 | #import "AuditTokenHack.h"
10 |
11 | @implementation AuditTokenHack
12 |
13 | + (NSData *)getAuditTokenDataFromNSXPCConnection:(NSXPCConnection *)connection {
14 | audit_token_t auditToken = connection.auditToken;
15 | return [NSData dataWithBytes:&auditToken length:sizeof(audit_token_t)];
16 | }
17 |
18 | @end
19 |
--------------------------------------------------------------------------------
/src/SupportHelper/ConnectionIdentityService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConnectionIdentityService.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ConnectionIdentityService {
11 |
12 | static private let requirementString =
13 | #"anchor apple generic and identifier "\#(HelperConstants.mainAppBundleID)" and certificate leaf[subject.OU] = "\#(HelperConstants.teamID)""# as CFString
14 |
15 | static func isConnectionValid(connection: NSXPCConnection) -> Bool {
16 | // 1
17 | guard let token = AuditTokenHack.getAuditTokenData(from: connection) else {
18 | logger.error("Unable to get the property 'auditToken' from the connection")
19 | return false
20 | }
21 |
22 | // 2
23 | guard let secCode = secCodeFrom(token: token), verifyWithRequirementString(secCode: secCode) else {
24 | return false
25 | }
26 |
27 | // 3
28 | return true
29 | }
30 |
31 | private static func secCodeFrom(token: Data) -> SecCode? {
32 | // 1
33 | let attributesDict = [kSecGuestAttributeAudit: token]
34 | var secCode: SecCode?
35 |
36 | // 2
37 | let status = SecCodeCopyGuestWithAttributes(
38 | nil,
39 | attributesDict as CFDictionary,
40 | SecCSFlags(rawValue: 0),
41 | &secCode
42 | )
43 |
44 | // 3
45 | if status.hasSecError {
46 | // unable to get the (running) code from the token
47 | logger.error("Could not get 'secCode' with the audit token. \(status.secErrorDescription, privacy: .public)")
48 | return nil
49 | }
50 |
51 | // 4
52 | return secCode
53 | }
54 |
55 | static private func verifyWithRequirementString(secCode: SecCode) -> Bool {
56 | var secRequirement: SecRequirement?
57 |
58 | // 1
59 | let reqStatus = SecRequirementCreateWithString(
60 | requirementString,
61 | SecCSFlags(rawValue: 0),
62 | &secRequirement
63 | )
64 |
65 | // 2
66 | if reqStatus.hasSecError {
67 | logger.error("Unable to create the requirement string. \(reqStatus.secErrorDescription, privacy: .public)")
68 | return false
69 | }
70 |
71 | // 3
72 | let validityStatus = SecCodeCheckValidity(
73 | secCode,
74 | SecCSFlags(rawValue: 0),
75 | secRequirement
76 | )
77 |
78 | // 4
79 | if validityStatus.hasSecError {
80 | logger.error("NSXPC client does not meet the requirements. \(reqStatus.secErrorDescription, privacy: .public)")
81 | return false
82 | }
83 |
84 | return true
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/src/SupportHelper/HelperConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelperConstants.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum HelperConstants {
11 | static let helpersFolder = "/Library/PrivilegedHelperTools/"
12 | static let domain = "nl.root3.support.helper"
13 | static let helperPath = helpersFolder + domain
14 | static let mainAppBundleID = "nl.root3.support"
15 | static let teamID = "98LJ4XBGYK"
16 | }
17 |
--------------------------------------------------------------------------------
/src/SupportHelper/HelperExecutionService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelperExecutionService.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct HelperExecutionService {
11 |
12 | static func executeScript(command: String, completion: @escaping ((NSNumber) -> Void)) throws -> Void {
13 |
14 | let process = Process()
15 | process.launchPath = "/bin/zsh"
16 | process.arguments = ["-c", "'\(command.removeEscapingCharacters())'"]
17 |
18 | let outputPipe = Pipe()
19 | process.standardOutput = outputPipe
20 | try process.run()
21 |
22 | // Stream script output to Unified Logging
23 | outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in
24 | let data = fileHandle.availableData
25 | if data.isEmpty {
26 | outputPipe.fileHandleForReading.readabilityHandler = nil
27 | return
28 | }
29 | if let outputString = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .newlines) {
30 | logger.log("\(outputString, privacy: .public)")
31 | print(outputString)
32 | }
33 | }
34 |
35 | process.waitUntilExit()
36 |
37 | process.terminationHandler = { process in
38 | completion(NSNumber(value: process.terminationStatus))
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/SupportHelper/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleShortVersionString
6 | 2.6.3
7 | NSHumanReadableCopyright
8 | © 2025 Root3 B.V. All rights reserved.
9 | CFBundleIdentifier
10 | nl.root3.support.helper
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | SupportAppPriviligedHelper
15 | CFBundleVersion
16 | 66
17 | SMAuthorizedClients
18 |
19 | anchor apple generic and identifier "nl.root3.support" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "98LJ4XBGYK")
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/SupportHelper/OSStatus+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OSStatus+Extensions.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension OSStatus {
11 |
12 | var hasSecError: Bool { self != errSecSuccess }
13 |
14 | var secErrorDescription: String {
15 | let error = SecCopyErrorMessageString(self, nil) as String? ?? "Unknown error"
16 | return error
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/SupportHelper/PrivilegedHelperError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrivilegedHelperError.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum PrivilegedHelperError: LocalizedError {
11 | case invalidStringConversion
12 | case helperInstallation(String)
13 | case helperConnection(String)
14 | case unknown
15 |
16 | var errorDescription: String? {
17 | switch self {
18 | case .invalidStringConversion: return "The output data is not convertible to a String (utf8)"
19 | case .helperInstallation(let description): return "Helper installation error. \(description)"
20 | case .helperConnection(let description): return "Helper connection error. \(description)"
21 | case .unknown: return "Unknown error"
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/SupportHelper/RemoteApplicationProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RemoteApplicationProtocol.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | @objc(MainApplicationProtocol)
11 | public protocol RemoteApplicationProtocol {
12 | // empty protocol but required for the XPC connection
13 | }
14 |
--------------------------------------------------------------------------------
/src/SupportHelper/SupportHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SupportHelper.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | class SupportHelper: NSObject, NSXPCListenerDelegate, SupportHelperProtocol {
11 |
12 | // MARK: - Properties
13 |
14 | let listener: NSXPCListener
15 | private var connections = [NSXPCConnection]()
16 | private var shouldQuit = false
17 | private var shouldQuitCheckInterval = 1.0
18 |
19 | // Support main app Code Requirement
20 | let codeRequirement = "anchor apple generic and identifier \"" + HelperConstants.mainAppBundleID + "\"" +
21 | " and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */" +
22 | " or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */" +
23 | " and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */" +
24 | " and certificate leaf[subject.OU] = \"" + HelperConstants.teamID + "\"" +
25 | ")"
26 |
27 | // MARK: - Initialisation
28 |
29 | override init() {
30 | self.listener = NSXPCListener(machServiceName: HelperConstants.domain)
31 | super.init()
32 | self.listener.delegate = self
33 | }
34 |
35 | // MARK: - Functions
36 |
37 | // MARK: HelperProtocol
38 |
39 | func executeScript(command: String, completion: @escaping (NSNumber) -> Void) {
40 |
41 | do {
42 | try HelperExecutionService.executeScript(command: command) { (result) in
43 | completion(result)
44 | }
45 | } catch {
46 | logger.error("Error: \(error.localizedDescription, privacy: .public)")
47 | }
48 | }
49 |
50 | func run() {
51 | // start listening on new connections
52 | self.listener.resume()
53 |
54 | // // prevent the terminal application to exit
55 | // RunLoop.current.run()
56 |
57 | // Keep the helper running until shouldQuit variable is set to true.
58 | // This variable is changed to true in the connection invalidation handler in the listener(_ listener:shoudlAcceptNewConnection:) function.
59 | while !shouldQuit {
60 | RunLoop.current.run(until: Date.init(timeIntervalSinceNow: shouldQuitCheckInterval))
61 | }
62 | }
63 |
64 | // Listen and monitor connections. When there are no more connections, the Runloop will stop running and the PrivilegedHelperTool will quit
65 | // https://github.com/choco/FlixDNS/blob/master/FlixDNS%20Privileged%20Helper/PrivilegedHelperService.swift
66 | func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
67 |
68 | // Make sure only the main app can connect and validate Team ID and Code Requirement
69 | guard ConnectionIdentityService.isConnectionValid(connection: newConnection) else {
70 | self.shouldQuit = true
71 | return false
72 | }
73 |
74 | newConnection.exportedInterface = NSXPCInterface(with: SupportHelperProtocol.self)
75 | newConnection.remoteObjectInterface = NSXPCInterface(with: RemoteApplicationProtocol.self)
76 |
77 | // Check Code Requirement
78 | if #available(macOS 13.0, *) {
79 | newConnection.setCodeSigningRequirement(codeRequirement)
80 | }
81 |
82 | newConnection.exportedObject = self
83 | newConnection.invalidationHandler = (() -> Void)? {
84 | if let indexValue = self.connections.firstIndex(of: newConnection) {
85 | self.connections.remove(at: indexValue)
86 | }
87 |
88 | if self.connections.count == 0 {
89 | logger.debug("No more XPC connections, exiting...")
90 | self.shouldQuit = true
91 | }
92 | }
93 | self.connections.append(newConnection)
94 | newConnection.resume()
95 | return true
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/SupportHelper/SupportHelperProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SupportHelperProtocol.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | /// The protocol that this service will vend as its API. This protocol will also need to be visible to the process hosting the service.
11 | @objc(SupportHelperProtocol)
12 | protocol SupportHelperProtocol {
13 |
14 | @objc func executeScript(command: String, completion: @escaping ((NSNumber) -> Void)) -> Void
15 | }
16 |
--------------------------------------------------------------------------------
/src/SupportHelper/launchd.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AssociatedBundleIdentifiers
6 |
7 | nl.root3.support
8 |
9 | Label
10 | nl.root3.support.helper
11 | MachServices
12 |
13 | nl.root3.support.helper
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/SupportHelper/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // nl.root3.support.helper
4 | //
5 | // Created by Jordy Witteman on 18/11/2023.
6 | //
7 |
8 | import Foundation
9 | import os
10 |
11 | var logger = Logger(subsystem: "nl.root3.support.helper", category: "SupportHelper")
12 |
13 | logger.debug("Support App Privileged Helper started")
14 | let supportHelper = SupportHelper()
15 | supportHelper.run()
16 |
--------------------------------------------------------------------------------
/src/SupportHelper/nl.root3.support.helper-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import "AuditTokenHack.h"
6 |
--------------------------------------------------------------------------------
/src/SupportXPC/ExecutionService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExecutionService.swift
3 | // SupportXPC
4 | //
5 | // Created by Jordy Witteman on 29/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ExecutionService {
11 |
12 | static func verifyAppCatalogCodeRequirement(completion: @escaping (Bool) -> Void) throws -> Void {
13 |
14 | let bundleIdentifier = "nl.root3.catalog.agent"
15 | let teamID = "98LJ4XBGYK"
16 |
17 | // Define the code requirement string
18 | let codeRequirementString = "anchor apple generic and identifier \"" + bundleIdentifier + "\"" +
19 | " and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */" +
20 | " or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */" +
21 | " and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */" +
22 | " and certificate leaf[subject.OU] = \"" + teamID + "\"" +
23 | ")"
24 |
25 | // Create a SecRequirementRef from the code requirement string
26 | var requirement: SecRequirement?
27 | let status = SecRequirementCreateWithString(codeRequirementString as CFString, [], &requirement)
28 |
29 | guard status == errSecSuccess, let codeRequirement = requirement else {
30 | logger.error("Error creating code requirement: \(status)")
31 | completion(false)
32 | return
33 | }
34 |
35 | // Get the URL of the binary to verify
36 | let symlinkURL = URL(fileURLWithPath: "/usr/local/bin/catalog")
37 |
38 | // Resolve the symlink to its target
39 | guard let binaryURL = try? FileManager.default.destinationOfSymbolicLink(atPath: symlinkURL.path) else {
40 | print("Error resolving symlink.")
41 | return
42 | }
43 |
44 | // Create a SecStaticCodeRef from the binary URL
45 | var staticCode: SecStaticCode?
46 | let staticCodeStatus = SecStaticCodeCreateWithPath(URL(fileURLWithPath: binaryURL) as CFURL, [], &staticCode)
47 |
48 | guard staticCodeStatus == errSecSuccess, let code = staticCode else {
49 | logger.error("Error creating static code: \(staticCodeStatus)")
50 | completion(false)
51 | return
52 | }
53 |
54 | // Check if the binary meets the code requirements
55 | let satisfiesRequirements = SecStaticCodeCheckValidityWithErrors(code, [], codeRequirement, nil)
56 |
57 | if satisfiesRequirements == errSecSuccess {
58 | logger.debug("Catalog binary meets code requirements")
59 | completion(true)
60 | } else {
61 | logger.error("Catalog binary does not meet code requirements")
62 | completion(false)
63 | }
64 |
65 | }
66 |
67 | static func getUpdateDeclaration(completion: @escaping (Data) -> Void) throws -> Void {
68 |
69 | // Specify the path to the plist file
70 | let plistPath = "/private/var/db/softwareupdate/SoftwareUpdateDDMStatePersistence.plist" // Replace this with the actual path to your plist file
71 |
72 | // Check if the file exists
73 | if FileManager.default.fileExists(atPath: plistPath) {
74 | logger.debug("macOS software update declaration plist was found")
75 |
76 | // Read the plist file
77 | do {
78 | // Read plist data from the file
79 | let plistData = try Data(contentsOf: URL(fileURLWithPath: plistPath))
80 | completion(plistData)
81 |
82 | } catch {
83 | logger.error("Error reading plist: \(error)")
84 | }
85 | } else {
86 | logger.debug("No macOS software update declaration plist found")
87 | }
88 | }
89 |
90 | static func executeScript(command: String, completion: @escaping ((NSNumber) -> Void)) throws -> Void {
91 |
92 | let process = Process()
93 | process.launchPath = "/bin/zsh"
94 | process.arguments = ["-c", "'\(command.removeEscapingCharacters())'"]
95 |
96 | let outputPipe = Pipe()
97 | process.standardOutput = outputPipe
98 | try process.run()
99 |
100 | // Stream script output to Unified Logging
101 | outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in
102 | let data = fileHandle.availableData
103 | if data.isEmpty {
104 | outputPipe.fileHandleForReading.readabilityHandler = nil
105 | return
106 | }
107 | if let outputString = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .newlines) {
108 | logger.log("\(outputString, privacy: .public)")
109 | print(outputString)
110 | }
111 | }
112 |
113 | process.waitUntilExit()
114 |
115 | process.terminationHandler = { process in
116 | completion(NSNumber(value: process.terminationStatus))
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/SupportXPC/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleShortVersionString
6 | 2.6.3
7 | CFBundleIdentifier
8 | $(PRODUCT_BUNDLE_IDENTIFIER)
9 | CFBundleName
10 | $(PRODUCT_NAME)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | NSHumanReadableCopyright
14 | © 2024 Root3 B.V. All rights reserved.
15 | CFBundleExecutable
16 | $(EXECUTABLE_NAME)
17 | CFBundleVersion
18 | 66
19 | CFBundlePackageType
20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
21 | XPCService
22 |
23 | ServiceType
24 | Application
25 | RunLoopType
26 | NSRunLoop
27 | JoinExistingSession
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/SupportXPC/SupportXPC.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/SupportXPC/SupportXPC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SupportXPC.swift
3 | // SupportXPC
4 | //
5 | // Created by Jordy Witteman on 27/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection.
11 | class SupportXPC: NSObject, SupportXPCProtocol {
12 |
13 | // MARK: HelperProtocol
14 | func executeScript(command: String, completion: @escaping (NSNumber) -> Void) {
15 |
16 | do {
17 | try ExecutionService.executeScript(command: command) { (result) in
18 | completion(result)
19 | }
20 | } catch {
21 | logger.error("Error: \(error.localizedDescription, privacy: .public)")
22 | }
23 | }
24 |
25 | func getUpdateDeclaration(completion: @escaping (Data) -> Void) {
26 |
27 | do {
28 | try ExecutionService.getUpdateDeclaration() { (result) in
29 | completion(result)
30 | }
31 | } catch {
32 | logger.error("Error: \(error.localizedDescription, privacy: .public)")
33 | }
34 |
35 | }
36 |
37 | func verifyAppCatalogCodeRequirement(completion: @escaping (Bool) -> Void) {
38 |
39 | do {
40 | try ExecutionService.verifyAppCatalogCodeRequirement() { (result) in
41 | completion(result)
42 | }
43 | } catch {
44 | logger.error("Error: \(error.localizedDescription, privacy: .public)")
45 | }
46 |
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/SupportXPC/SupportXPCProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SupportXPCProtocol.swift
3 | // SupportXPC
4 | //
5 | // Created by Jordy Witteman on 27/11/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | /// The protocol that this service will vend as its API. This protocol will also need to be visible to the process hosting the service.
11 | @objc protocol SupportXPCProtocol {
12 |
13 | /// Replace the API of this protocol with an API appropriate to the service you are vending.
14 | @objc func executeScript(command: String, completion: @escaping ((NSNumber) -> Void)) -> Void
15 |
16 | @objc func getUpdateDeclaration(completion: @escaping (Data) -> Void) -> Void
17 |
18 | @objc func verifyAppCatalogCodeRequirement(completion: @escaping (Bool) -> Void) -> Void
19 | }
20 |
21 | /*
22 | To use the service from an application or other process, use NSXPCConnection to establish a connection to the service by doing something like this:
23 |
24 | connectionToService = NSXPCConnection(serviceName: "nl.root3.support.xpc")
25 | connectionToService.remoteObjectInterface = NSXPCInterface(with: SupportXPCProtocol.self)
26 | connectionToService.resume()
27 |
28 | Once you have a connection to the service, you can use it like this:
29 |
30 | if let proxy = connectionToService.remoteObjectProxy as? SupportXPCProtocol {
31 | proxy.performCalculation(firstNumber: 23, secondNumber: 19) { result in
32 | NSLog("Result of calculation is: \(result)")
33 | }
34 | }
35 |
36 | And, when you are finished with the service, clean up the connection like this:
37 |
38 | connectionToService.invalidate()
39 | */
40 |
--------------------------------------------------------------------------------
/src/SupportXPC/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // SupportXPC
4 | //
5 | // Created by Jordy Witteman on 27/11/2023.
6 | //
7 |
8 | import Foundation
9 | import os
10 |
11 | var logger = Logger(subsystem: "nl.root3.support.xpc", category: "SupportXPC")
12 |
13 | class ServiceDelegate: NSObject, NSXPCListenerDelegate {
14 |
15 | /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection.
16 | func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
17 |
18 | // Configure the connection.
19 | // First, set the interface that the exported object implements.
20 | newConnection.exportedInterface = NSXPCInterface(with: SupportXPCProtocol.self)
21 |
22 | // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object.
23 | let exportedObject = SupportXPC()
24 | newConnection.exportedObject = exportedObject
25 |
26 | // Resuming the connection allows the system to deliver more incoming messages.
27 | newConnection.resume()
28 |
29 | // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false.
30 | return true
31 | }
32 | }
33 |
34 | // Create the delegate for the service.
35 | let delegate = ServiceDelegate()
36 |
37 | // Set up the one NSXPCListener for this service. It will handle all incoming connections.
38 | let listener = NSXPCListener.service()
39 | listener.delegate = delegate
40 |
41 | // Resuming the serviceListener starts this service. This method does not return.
42 | listener.resume()
43 |
--------------------------------------------------------------------------------