├── .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 | --------------------------------------------------------------------------------