├── .github └── workflows │ ├── build_nudge_pr.yml │ ├── build_nudge_prerelease.yml │ ├── build_nudge_prerelease_manual.yml │ ├── build_nudge_release.yml │ └── build_nudge_release_manual.yml ├── .gitignore ├── CHANGELOG.md ├── Example Assets ├── com.github.macadmins.Nudge.json ├── com.github.macadmins.Nudge.mobileconfig ├── com.github.macadmins.Nudge.tester.json └── com.github.macadmins.Nudge.tester.plist ├── LICENSE ├── Localizable.xcstrings ├── Nudge.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ ├── Nudge - Debug (-bundle-mode-json).xcscheme │ │ ├── Nudge - Debug (-bundle-mode-json, -simulate-os-version, -simulate-hardware-id).xcscheme │ │ ├── Nudge - Debug (-bundle-mode-profile).xcscheme │ │ ├── Nudge - Debug (-demo-mode).xcscheme │ │ ├── Nudge - Debug (-demo-mode, -debug-ui-mode, -force-screenshot-icon).xcscheme │ │ ├── Nudge - Debug (-demo-mode, -force-screenshot-icon).xcscheme │ │ ├── Nudge - Debug (-demo-mode, -simple-mode).xcscheme │ │ ├── Nudge - Debug (-simple-mode).xcscheme │ │ ├── Nudge - Debug.xcscheme │ │ └── Nudge - Release.xcscheme └── xcuserdata │ └── erikg.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── Nudge ├── 3rd Party Assets │ ├── gdmf.swift │ └── sofa.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── CompanyScreenshotIcon.imageset │ │ ├── Contents.json │ │ ├── GenericLogo_Normal.png │ │ └── GenericLogo_Normal@2x.png │ ├── Contents.json │ └── ProductPageIcon.imageset │ │ └── Contents.json ├── Info.plist ├── Nudge-Debug.entitlements ├── Nudge.entitlements ├── Preferences │ ├── DefaultPreferencesNudge.swift │ └── PreferencesStructure.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Scripts │ ├── postinstall-launchagent │ ├── postinstall-logger │ └── postinstall-nudge ├── UI │ ├── Common │ │ ├── AdditionalInfoButton.swift │ │ ├── BackgroundBlur.swift │ │ ├── CloseButton.swift │ │ ├── CompanyLogo.swift │ │ ├── DeferView.swift │ │ ├── DeviceInfo.swift │ │ ├── InformationButton.swift │ │ └── QuitButtons.swift │ ├── Defaults.swift │ ├── Main.swift │ ├── SimpleMode │ │ └── SimpleMode.swift │ └── StandardMode │ │ ├── LeftSide.swift │ │ ├── RightSide.swift │ │ ├── ScreenShotZoom.swift │ │ └── StandardMode.swift ├── Utilities │ ├── Extensions.swift │ ├── Logger.swift │ ├── OSVersion.swift │ ├── Preferences.swift │ ├── SoftwareUpdate.swift │ ├── UILogic.swift │ └── Utils.swift └── com.github.macadmins.Nudge.SMAppService.plist ├── NudgeTests ├── Info.plist ├── NudgeTests.swift └── OSVersionTests.swift ├── NudgeUITests ├── Info.plist └── NudgeUITests.swift ├── README.md ├── SECURITY.md ├── Schema └── jamf │ └── com.github.macadmins.Nudge.json ├── assets ├── NudgeCustomLogoGuide.png ├── NudgeCustomLogoGuide.pxm ├── NudgeIcon.png ├── NudgeIcon.pxm ├── NudgeIconInverted.png ├── simple_mode │ ├── demo_simple_dark_1.png │ ├── demo_simple_dark_2.png │ ├── demo_simple_dark_localized.png │ ├── demo_simple_light_1.png │ ├── demo_simple_light_2.png │ └── demo_simple_light_localized.png └── standard_mode │ ├── demo_dark_1_icon.png │ ├── demo_dark_1_no_icon.png │ ├── demo_dark_2_icon.png │ ├── demo_dark_2_icon_localized.png │ ├── demo_dark_2_no_icon.png │ ├── demo_dark_3.png │ ├── demo_dark_4.png │ ├── demo_dark_4_localized.png │ ├── demo_light_1_icon.png │ ├── demo_light_1_no_icon.png │ ├── demo_light_2_icon.png │ ├── demo_light_2_localized.png │ ├── demo_light_2_no_icon.png │ ├── demo_light_3.png │ ├── demo_light_4.png │ └── demo_light_4_icon_localized.png ├── build_assets ├── com.github.macadmins.Nudge.logger.plist ├── com.github.macadmins.Nudge.plist ├── postinstall-essentials ├── postinstall-launchagent ├── postinstall-logger ├── postinstall-nudge └── postinstall-suite └── build_nudge.zsh /.github/workflows/build_nudge_pr.yml: -------------------------------------------------------------------------------- 1 | name: Build signed Nudge for pull requests 2 | 3 | on: 4 | pull_request_target: 5 | types: [labeled] 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-14 10 | if: contains(github.event.pull_request.labels.*.name, 'safe-to-test') 11 | 12 | steps: 13 | - name: Checkout nudge repo 14 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 15 | with: 16 | ref: ${{ github.event.pull_request.head.sha }} 17 | fetch-depth: 0 18 | 19 | - name: Install Apple Xcode certificates 20 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 21 | with: 22 | keychain-password: ${{ github.run_id }} 23 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }} 24 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }} 25 | 26 | - name: Install Apple Installer certificates 27 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 28 | with: 29 | create-keychain: false # do not create a new keychain for this value 30 | keychain-password: ${{ github.run_id }} 31 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }} 32 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }} 33 | 34 | - name: Run build script 35 | run: ./build_nudge.zsh 36 | 37 | - name: get environment variables 38 | id: get_env_var 39 | run: | 40 | echo "NUDGE_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV 41 | echo "NUDGE_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV 42 | 43 | - name: Upload zip archive 44 | uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 45 | with: 46 | name: packages 47 | path: outputs/ 48 | 49 | - name: Clean up PR safety tag 50 | if: always() 51 | id: clean_up_pr_safety_tag 52 | run: | 53 | /usr/bin/curl --silent --fail-with-body -X DELETE -H 'Accept: application/vnd.github.v3+json' -H 'Authorization: token ${{ github.token }}' 'https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.number }}/labels/safe-to-test' 54 | -------------------------------------------------------------------------------- /.github/workflows/build_nudge_prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Build signed Nudge and upload signed package (prerelease) 2 | 3 | env: 4 | NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }} 5 | 6 | on: 7 | push: 8 | branches: 9 | - development 10 | paths-ignore: 11 | - 'assets/**' 12 | - 'Example Assets/**' 13 | - '**/CHANGELOG.md' 14 | - '**/README.md' 15 | 16 | jobs: 17 | build: 18 | runs-on: macos-14 19 | 20 | steps: 21 | - name: Checkout nudge repo 22 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Install Apple Xcode certificates 27 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 28 | with: 29 | keychain-password: ${{ github.run_id }} 30 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }} 31 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }} 32 | 33 | - name: Install Apple Installer certificates 34 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 35 | with: 36 | create-keychain: false # do not create a new keychain for this value 37 | keychain-password: ${{ github.run_id }} 38 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }} 39 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }} 40 | 41 | - name: Run build package script 42 | run: ./build_nudge.zsh "CREATE_PKG" "$NOTARY_APP_PASSWORD" 43 | 44 | - name: get environment variables 45 | id: get_env_var 46 | run: | 47 | echo "NUDGE_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV 48 | echo "NUDGE_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV 49 | 50 | - name: Get Changelog Entry 51 | id: changelog_reader 52 | uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2 53 | with: 54 | validation_depth: 100 55 | version: ${{ env.NUDGE_MAIN_VERSION }} 56 | 57 | - name: Generate changelog 58 | id: changelog 59 | uses: metcalfc/changelog-generator@afdcb9470aebdb2252c0c95a1c130723c9e21f3a # v4.1 60 | with: 61 | myToken: ${{ secrets.GITHUB_TOKEN }} 62 | reverse: 'true' 63 | 64 | - name: Create Pre-Release 65 | id: create_pre_release 66 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 67 | with: 68 | name: Nudge ${{env.NUDGE_VERSION}} 69 | tag_name: v${{env.NUDGE_VERSION}} 70 | draft: false 71 | prerelease: true 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | body: | 74 | # Notes 75 | This is a pre-release version of Nudge created by GitHub Actions. 76 | Nudge.app has been signed and notarized. The package has been signed, notarized and stapled. 77 | 78 | By default Nudge looks for a `com.github.macadmins.Nudge.json` file located in `/Library/Preferences`. If you would like to use an alternative path, please read the [README](https://github.com/macadmins/nudge/blob/main/README.md) or the [WIKI](https://github.com/macadmins/nudge/wiki) 79 | 80 | ## About the LaunchAgent 81 | This is a basic launch agent that opens Nudge twice an hour, every 30 minutes. 82 | If you would like to reduce the amount of times Nudge launches per day, it is recommended to create your own LaunchAgent. 83 | 84 | # Changelog 85 | ${{ steps.changelog_reader.outputs.changes }} 86 | 87 | # Changes 88 | ${{ steps.changelog.outputs.changelog }} 89 | files: ${{github.workspace}}/outputs/*.pkg 90 | 91 | - name: Upload packages 92 | uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 93 | with: 94 | name: packages 95 | path: outputs/ 96 | -------------------------------------------------------------------------------- /.github/workflows/build_nudge_prerelease_manual.yml: -------------------------------------------------------------------------------- 1 | name: Manual build signed Nudge and upload signed package (prerelease) 2 | 3 | env: 4 | NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }} 5 | 6 | on: [workflow_dispatch] 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-14 11 | 12 | steps: 13 | - name: Checkout nudge repo 14 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install Apple Xcode certificates 19 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 20 | with: 21 | keychain-password: ${{ github.run_id }} 22 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }} 23 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }} 24 | 25 | - name: Install Apple Installer certificates 26 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 27 | with: 28 | create-keychain: false # do not create a new keychain for this value 29 | keychain-password: ${{ github.run_id }} 30 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }} 31 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }} 32 | 33 | - name: Run build package script 34 | run: ./build_nudge.zsh "CREATE_PKG" "$NOTARY_APP_PASSWORD" 35 | 36 | - name: get environment variables 37 | id: get_env_var 38 | run: | 39 | echo "NUDGE_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV 40 | echo "NUDGE_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV 41 | 42 | - name: Get Changelog Entry 43 | id: changelog_reader 44 | uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2 45 | with: 46 | validation_depth: 100 47 | version: ${{ env.NUDGE_MAIN_VERSION }} 48 | 49 | - name: Generate changelog 50 | id: changelog 51 | uses: metcalfc/changelog-generator@afdcb9470aebdb2252c0c95a1c130723c9e21f3a # v4.1 52 | with: 53 | myToken: ${{ secrets.GITHUB_TOKEN }} 54 | reverse: 'true' 55 | 56 | - name: Create Pre-Release 57 | id: create_pre_release 58 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 59 | with: 60 | name: Nudge ${{env.NUDGE_VERSION}} 61 | tag_name: v${{env.NUDGE_VERSION}} 62 | draft: false 63 | prerelease: true 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | body: | 66 | # Notes 67 | This is a pre-release version of Nudge created by GitHub Actions. 68 | Nudge.app has been signed and notarized. The package has been signed, notarized and stapled. 69 | 70 | By default Nudge looks for a `com.github.macadmins.Nudge.json` file located in `/Library/Preferences`. If you would like to use an alternative path, please read the [README](https://github.com/macadmins/nudge/blob/main/README.md) or the [WIKI](https://github.com/macadmins/nudge/wiki) 71 | 72 | ## About the LaunchAgent 73 | This is a basic launch agent that opens Nudge twice an hour, every 30 minutes. 74 | If you would like to reduce the amount of times Nudge launches per day, it is recommended to create your own LaunchAgent. 75 | 76 | # Changelog 77 | ${{ steps.changelog_reader.outputs.changes }} 78 | 79 | # Changes 80 | ${{ steps.changelog.outputs.changelog }} 81 | files: ${{github.workspace}}/outputs/*.pkg 82 | 83 | - name: Upload packages 84 | uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 85 | with: 86 | name: packages 87 | path: outputs/ 88 | -------------------------------------------------------------------------------- /.github/workflows/build_nudge_release.yml: -------------------------------------------------------------------------------- 1 | name: Build signed Nudge and upload signed package 2 | 3 | env: 4 | NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }} 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - 'assets/**' 12 | - 'Example Assets/**' 13 | - '**/CHANGELOG.md' 14 | - '**/README.md' 15 | 16 | jobs: 17 | build: 18 | runs-on: macos-14 19 | 20 | steps: 21 | - name: Checkout nudge repo 22 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Install Apple Xcode certificates 27 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 28 | with: 29 | keychain-password: ${{ github.run_id }} 30 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }} 31 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }} 32 | 33 | - name: Install Apple Installer certificates 34 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 35 | with: 36 | create-keychain: false # do not create a new keychain for this value 37 | keychain-password: ${{ github.run_id }} 38 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }} 39 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }} 40 | 41 | - name: Run build package script 42 | run: ./build_nudge.zsh "CREATE_PKG" "$NOTARY_APP_PASSWORD" 43 | 44 | - name: get environment variables 45 | id: get_env_var 46 | run: | 47 | echo "NUDGE_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV 48 | echo "NUDGE_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV 49 | 50 | - name: Get Changelog Entry 51 | id: changelog_reader 52 | uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2 53 | with: 54 | validation_depth: 100 55 | version: ${{ env.NUDGE_MAIN_VERSION }} 56 | 57 | - name: Generate changelog 58 | id: changelog 59 | uses: metcalfc/changelog-generator@afdcb9470aebdb2252c0c95a1c130723c9e21f3a # v4.1 60 | with: 61 | myToken: ${{ secrets.GITHUB_TOKEN }} 62 | reverse: 'true' 63 | 64 | - name: Create Release 65 | id: create_release 66 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 67 | with: 68 | name: Nudge ${{env.NUDGE_VERSION}} 69 | tag_name: v${{env.NUDGE_VERSION}} 70 | draft: false 71 | prerelease: false 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | body: | 74 | # Notes 75 | This is a version of Nudge created by GitHub Actions. 76 | Nudge.app has been signed and notarized. The package has been signed, notarized and stapled. 77 | 78 | By default Nudge looks for a `com.github.macadmins.Nudge.json` file located in `/Library/Preferences`. If you would like to use an alternative path, please read the [README](https://github.com/macadmins/nudge/blob/main/README.md) or the [WIKI](https://github.com/macadmins/nudge/wiki) 79 | 80 | ## About the LaunchAgent 81 | This is a basic launch agent that opens Nudge twice an hour, every 30 minutes. 82 | If you would like to reduce the amount of times Nudge launches per day, it is recommended to create your own LaunchAgent. 83 | 84 | # Changelog 85 | ${{ steps.changelog_reader.outputs.changes }} 86 | 87 | # Changes 88 | ${{ steps.changelog.outputs.changelog }} 89 | files: ${{github.workspace}}/outputs/*.pkg 90 | 91 | - name: Upload packages 92 | uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 93 | with: 94 | name: packages 95 | path: outputs/ 96 | -------------------------------------------------------------------------------- /.github/workflows/build_nudge_release_manual.yml: -------------------------------------------------------------------------------- 1 | name: Manual build signed Nudge and upload signed package 2 | 3 | env: 4 | NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }} 5 | 6 | on: [workflow_dispatch] 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-14 11 | 12 | steps: 13 | - name: Checkout nudge repo 14 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install Apple Xcode certificates 19 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 20 | with: 21 | keychain-password: ${{ github.run_id }} 22 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }} 23 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }} 24 | 25 | - name: Install Apple Installer certificates 26 | uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d # v3.0.0 27 | with: 28 | create-keychain: false # do not create a new keychain for this value 29 | keychain-password: ${{ github.run_id }} 30 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }} 31 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }} 32 | 33 | - name: Run build package script 34 | run: ./build_nudge.zsh "CREATE_PKG" "$NOTARY_APP_PASSWORD" 35 | 36 | - name: get environment variables 37 | id: get_env_var 38 | run: | 39 | echo "NUDGE_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV 40 | echo "NUDGE_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV 41 | 42 | - name: Get Changelog Entry 43 | id: changelog_reader 44 | uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2 45 | with: 46 | validation_depth: 100 47 | version: ${{ env.NUDGE_MAIN_VERSION }} 48 | 49 | - name: Generate changelog 50 | id: changelog 51 | uses: metcalfc/changelog-generator@afdcb9470aebdb2252c0c95a1c130723c9e21f3a # v4.1 52 | with: 53 | myToken: ${{ secrets.GITHUB_TOKEN }} 54 | reverse: 'true' 55 | 56 | - name: Create Release 57 | id: create_release 58 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 59 | with: 60 | name: Nudge ${{env.NUDGE_VERSION}} 61 | tag_name: v${{env.NUDGE_VERSION}} 62 | draft: false 63 | prerelease: false 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | body: | 66 | # Notes 67 | This is a version of Nudge created by GitHub Actions. 68 | Nudge.app has been signed and notarized. The package has been signed, notarized and stapled. 69 | 70 | By default Nudge looks for a `com.github.macadmins.Nudge.json` file located in `/Library/Preferences`. If you would like to use an alternative path, please read the [README](https://github.com/macadmins/nudge/blob/main/README.md) or the [WIKI](https://github.com/macadmins/nudge/wiki) 71 | 72 | ## About the LaunchAgent 73 | This is a basic launch agent that opens Nudge twice an hour, every 30 minutes. 74 | If you would like to reduce the amount of times Nudge launches per day, it is recommended to create your own LaunchAgent. 75 | 76 | # Changelog 77 | ${{ steps.changelog_reader.outputs.changes }} 78 | 79 | # Changes 80 | ${{ steps.changelog.outputs.changelog }} 81 | files: ${{github.workspace}}/outputs/*.pkg 82 | 83 | - name: Upload packages 84 | uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 85 | with: 86 | name: packages 87 | path: outputs/ 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .DS_Store 93 | outputs/ 94 | NudgePkg/ 95 | NudgePkgLA/ 96 | NudgePkgLogger/ 97 | build_info.txt 98 | date_info.txt 99 | -------------------------------------------------------------------------------- /Example Assets/com.github.macadmins.Nudge.json: -------------------------------------------------------------------------------- 1 | { 2 | "optionalFeatures": { 3 | "acceptableApplicationBundleIDs": [], 4 | "acceptableAssertionUsage": false, 5 | "acceptableCameraUsage": false, 6 | "acceptableScreenSharingUsage": false, 7 | "aggressiveUserExperience": true, 8 | "aggressiveUserFullScreenExperience": true, 9 | "asynchronousSoftwareUpdate": true, 10 | "attemptToBlockApplicationLaunches": false, 11 | "attemptToFetchMajorUpgrade": true, 12 | "blockedApplicationBundleIDs": [], 13 | "enforceMinorUpdates": true, 14 | "terminateApplicationsOnLaunch": false 15 | }, 16 | "osVersionRequirements": [ 17 | { 18 | "aboutUpdateURL_disabled": "https://support.apple.com/en-us/HT211896#macos1121", 19 | "aboutUpdateURLs": [ 20 | { 21 | "_language": "en", 22 | "aboutUpdateURL": "https://support.apple.com/en-us/HT211896#macos1121" 23 | }, 24 | { 25 | "_language": "es", 26 | "aboutUpdateURL": "https://support.apple.com/es-es/HT211896" 27 | }, 28 | { 29 | "_language": "fr", 30 | "aboutUpdateURL": "https://support.apple.com/fr-fr/HT211896" 31 | }, 32 | { 33 | "_language": "de", 34 | "aboutUpdateURL": "https://support.apple.com/de-de/HT211896" 35 | } 36 | ], 37 | "actionButtonPath": "munki://updates", 38 | "majorUpgradeAppPath": "/Applications/Install macOS Big Sur.app", 39 | "requiredInstallationDate": "2021-08-28T00:00:00Z", 40 | "requiredMinimumOSVersion": "11.5.2", 41 | "targetedOSVersionsRule": "default" 42 | } 43 | ], 44 | "userExperience": { 45 | "allowGracePeriods": false, 46 | "allowLaterDeferralButton": true, 47 | "allowUserQuitDeferrals": true, 48 | "allowedDeferrals": 1000000, 49 | "allowedDeferralsUntilForcedSecondaryQuitButton": 14, 50 | "approachingRefreshCycle": 6000, 51 | "approachingWindowTime": 72, 52 | "calendarDeferralUnit": "imminentWindowTime", 53 | "elapsedRefreshCycle": 300, 54 | "gracePeriodInstallDelay": 23, 55 | "gracePeriodLaunchDelay": 1, 56 | "gracePeriodPath": "/private/var/db/.AppleSetupDone", 57 | "imminentRefreshCycle": 600, 58 | "imminentWindowTime": 24, 59 | "initialRefreshCycle": 18000, 60 | "launchAgentIdentifier": "com.github.macadmins.Nudge", 61 | "loadLaunchAgent": false, 62 | "maxRandomDelayInSeconds": 1200, 63 | "noTimers": false, 64 | "nudgeRefreshCycle": 60, 65 | "randomDelay": true 66 | }, 67 | "userInterface": { 68 | "actionButtonPath": "munki://updates", 69 | "fallbackLanguage": "en", 70 | "forceFallbackLanguage": false, 71 | "forceScreenShotIcon": false, 72 | "iconDarkPath": "/somewhere/logoDark.png", 73 | "iconLightPath": "/somewhere/logoLight.png", 74 | "screenShotDarkPath": "/somewhere/screenShotDark.png", 75 | "screenShotLightPath": "/somewhere/screenShotLight.png", 76 | "showDeferralCount": true, 77 | "simpleMode": false, 78 | "singleQuitButton": false, 79 | "updateElements": [ 80 | { 81 | "_language": "en", 82 | "actionButtonText": "Update Device", 83 | "customDeferralButtonText": "Custom", 84 | "customDeferralDropdownText": "Defer", 85 | "informationButtonText": "More Info", 86 | "mainContentHeader": "Your device will restart during this update", 87 | "mainContentNote": "Important Notes", 88 | "mainContentSubHeader": "Updates can take around 30 minutes to complete", 89 | "mainContentText": "A fully up-to-date device is required to ensure that IT can accurately protect your device.\n\nIf you do not update your device, you may lose access to some items necessary for your day-to-day tasks.\n\nTo begin the update, simply click on the Update Device button and follow the provided steps.", 90 | "mainHeader": "Your device requires a security update", 91 | "oneDayDeferralButtonText": "One Day", 92 | "oneHourDeferralButtonText": "One Hour", 93 | "primaryQuitButtonText": "Later", 94 | "secondaryQuitButtonText": "I understand", 95 | "subHeader": "A friendly reminder from your local IT team", 96 | "screenShotAltText": "Click to zoom into screenshot" 97 | }, 98 | { 99 | "_language": "es", 100 | "actionButtonText": "Actualizar dispositivo", 101 | "informationButtonText": "Más información", 102 | "mainContentHeader": "Su dispositivo se reiniciará durante esta actualización", 103 | "mainContentNote": "Notas importantes", 104 | "mainContentSubHeader": "Las actualizaciones pueden tardar unos 30 minutos en completarse", 105 | "mainContentText": "Se requiere un dispositivo completamente actualizado para garantizar que IT pueda proteger su dispositivo con precisión.\n\nSi no actualiza su dispositivo, es posible que pierda el acceso a algunos elementos necesarios para sus tareas diarias.\n\nPara comenzar la actualización, simplemente haga clic en el botón Actualizar dispositivo y siga los pasos proporcionados.", 106 | "mainHeader": "Tu dispositivo requiere una actualización de seguridad", 107 | "primaryQuitButtonText": "Más tarde", 108 | "secondaryQuitButtonText": "Entiendo", 109 | "subHeader": "Un recordatorio amistoso de su equipo de IT local", 110 | "screenShotAltText": "Haga clic para ampliar la captura de pantalla" 111 | }, 112 | { 113 | "_language": "fr", 114 | "actionButtonText": "Mettre à jour l'appareil", 115 | "informationButtonText": "Plus d'informations", 116 | "mainContentHeader": "Votre appareil redémarrera pendant cette mise à jour", 117 | "mainContentNote": "Notes Importantes", 118 | "mainContentSubHeader": "Les mises à jour peuvent prendre environ 30 minutes.", 119 | "mainContentText": "Un appareil entièrement à jour est nécessaire pour garantir que le service informatique puisse protéger votre appareil efficacement.\n\n Si vous ne mettez pas à jour votre appareil, vous risquez de perdre l'accès à certains outils nécessaires à vos tâches quotidiennes.\n\nPour commencer la mise à jour, cliquez simplement sur le bouton Mettre à jour le périphérique et suivez les étapes fournies.", 120 | "mainHeader": "Votre appareil nécessite une mise à jour de sécurité.", 121 | "primaryQuitButtonText": "Plus tard", 122 | "secondaryQuitButtonText": "Je comprends", 123 | "subHeader": "Un rappel amical de votre équipe informatique locale", 124 | "screenShotAltText": "Cliquez pour agrandir la capture d'écran" 125 | }, 126 | { 127 | "_language": "de", 128 | "actionButtonText": "Gerät aktualisieren", 129 | "informationButtonText": "Mehr Informationen", 130 | "mainContentHeader": "Ihr Gerät wird während dieses Updates neu gestartet", 131 | "mainContentNote": "Wichtige Hinweise", 132 | "mainContentSubHeader": "Aktualisierungen können ca. 30 Minuten dauern.", 133 | "mainContentText": "Ein vollständig aktualisiertes Gerät ist erforderlich, um sicherzustellen, dass die IT-Abteilung Ihr Gerät effektiv schützen kann.\n\nWenn Sie Ihr Gerät nicht aktualisieren, verlieren Sie möglicherweise den Zugriff auf einige Werkzeuge, die Sie für Ihre täglichen Aufgaben benötigen.\n\nUm das Update zu starten, klicken Sie auf die Schaltfläche Gerät aktualisieren und befolgen Sie die angegebenen Schritte.", 134 | "mainHeader": "Ihr Gerät benötigt ein Sicherheitsupdate", 135 | "primaryQuitButtonText": "Später", 136 | "secondaryQuitButtonText": "Ich verstehe", 137 | "subHeader": "Eine freundliche Erinnerung von Ihrem IT-Team", 138 | "screenShotAltText": "Hier klicken, um den Screenshot zu vergrößern" 139 | } 140 | ] 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Example Assets/com.github.macadmins.Nudge.tester.json: -------------------------------------------------------------------------------- 1 | { 2 | "optionalFeatures": { 3 | "customSOFAFeedURL": "https://sofafeed.macadmins.io/v1/macos_data_feed.json", 4 | "_customSOFAFeedURL": "https://sofafeed.macadmins.io/beta/v1/macos_data_feed.json", 5 | }, 6 | "osVersionRequirements": [ 7 | { 8 | "aboutUpdateURL": "sofa", 9 | "requiredMinimumOSVersion": "latest", 10 | "unsupportedURL": "https://google.com" 11 | } 12 | ], 13 | "userExperience": { 14 | "elapsedRefreshCycle": 10, 15 | "initialRefreshCycle": 10, 16 | "nudgeRefreshCycle": 5, 17 | "randomDelay": false 18 | }, 19 | "userInterface": { 20 | "iconDarkPath": "https://github.com/macadmins/nudge/blob/main/assets/NudgeIconInverted.png?raw=true", 21 | "iconLightPath": "https://github.com/macadmins/nudge/blob/main/assets/NudgeIcon.png?raw=true", 22 | "screenShotDarkPath": "https://github.com/macadmins/nudge/blob/main/assets/standard_mode/demo_dark_1_icon.png?raw=true", 23 | "screenShotLightPath": "https://github.com/macadmins/nudge/blob/main/assets/standard_mode/demo_light_1_icon.png?raw=true", 24 | "applicationTerminatedNotificationImagePath": "/Library/Application Support/Nudge/logoLight.png", 25 | "updateElements": [ 26 | { 27 | "_language": "en", 28 | "actionButtonText": "actionButtonText", 29 | "actionButtonTextUnsupported": "actionButtonTextUnsupported", 30 | "customDeferralButtonText": "customDeferralButtonText", 31 | "customDeferralDropdownText": "customDeferralDropdownText", 32 | "informationButtonText": "informationButtonText", 33 | "mainContentHeader": "mainContentHeader", 34 | "mainContentHeaderUnsupported": "mainContentHeaderUnsupported", 35 | "mainContentNote": "mainContentNote", 36 | "mainContentNoteUnsupported": "mainContentNoteUnsupported", 37 | "mainContentSubHeader": "mainContentSubHeader", 38 | "mainContentSubHeaderUnsupported1": "mainContentSubHeaderUnsupported", 39 | "mainContentText": "mainContentText", 40 | "mainContentTextUnsupported1": "mainContentTextUnsupported", 41 | "mainHeader": "mainHeader", 42 | "mainHeaderUnsupported": "mainHeaderUnsupported", 43 | "oneDayDeferralButtonText": "oneDayDeferralButtonText", 44 | "oneHourDeferralButtonText": "oneHourDeferralButtonText", 45 | "primaryQuitButtonText": "primaryQuitButtonText", 46 | "secondaryQuitButtonText": "secondaryQuitButtonText", 47 | "subHeader": "subHeader", 48 | "subHeaderUnsupported": "subHeaderUnsupported", 49 | "screenShotAltText": "Click to zoom into screenshot" 50 | } 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example Assets/com.github.macadmins.Nudge.tester.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | optionalFeatures 6 | 7 | acceptableAssertionApplicationNames 8 | 9 | zoom.us 10 | Meeting Center 11 | Google Chrome 12 | Safari 13 | 14 | acceptableAssertionUsage 15 | 16 | acceptableCameraUsage 17 | 18 | acceptableScreenSharingUsage 19 | 20 | attemptToBlockApplicationLaunches 21 | 22 | blockedApplicationBundleIDs 23 | 24 | com.microsoft.VSCode 25 | us.zoom.xos 26 | 27 | terminateApplicationsOnLaunch 28 | 29 | utilizeSOFAFeed 30 | 31 | 32 | osVersionRequirements 33 | 34 | 35 | aboutUpdateURL 36 | https://apple.com 37 | requiredMinimumOSVersion 38 | latest-supported 39 | unsupportedURL 40 | https://google.com 41 | 42 | 43 | userExperience 44 | 45 | elapsedRefreshCycle 46 | 10 47 | initialRefreshCycle 48 | 10 49 | loadLaunchAgent 50 | 51 | nudgeRefreshCycle 52 | 5 53 | randomDelay 54 | 55 | 56 | userInterface 57 | 58 | iconDarkPath 59 | https://github.com/macadmins/nudge/blob/main/assets/NudgeIconInverted.png?raw=true 60 | iconLightPath 61 | https://github.com/macadmins/nudge/blob/main/assets/NudgeIcon.png?raw=true 62 | screenShotDarkPath 63 | https://github.com/macadmins/nudge/blob/main/assets/standard_mode/demo_dark_1_icon.png?raw=true 64 | screenShotLightPath 65 | https://github.com/macadmins/nudge/blob/main/assets/standard_mode/demo_light_1_icon.png?raw=true 66 | simpleMode 67 | 68 | updateElements 69 | 70 | 71 | _language 72 | en 73 | actionButtonText 74 | actionButtonText 75 | customDeferralButtonText 76 | customDeferralButtonText 77 | customDeferralDropdownText 78 | customDeferralDropdownText 79 | informationButtonText 80 | informationButtonText 81 | actionButtonTextUnsupported 82 | actionButtonTextUnsupported 83 | mainContentHeader 84 | mainContentHeader 85 | mainContentHeaderUnsupported1 86 | mainContentHeaderUnsupported 87 | mainContentNote 88 | mainContentNote 89 | mainContentNoteUnsupported1 90 | mainContentNoteUnsupported 91 | mainContentSubHeader 92 | mainContentSubHeader 93 | mainContentSubHeaderUnsupported1 94 | mainContentSubHeaderUnsupported 95 | mainContentText 96 | mainContentText 97 | mainContentTextUnsupported1 98 | mainContentTextUnsupported 99 | mainHeader 100 | mainHeader 101 | mainHeaderUnsupported1 102 | mainHeaderUnsupported 103 | oneDayDeferralButtonText 104 | oneDayDeferralButtonText 105 | oneHourDeferralButtonText 106 | oneHourDeferralButtonText 107 | primaryQuitButtonText 108 | primaryQuitButtonText 109 | screenShotAltText 110 | Click to zoom into screenshot 111 | secondaryQuitButtonText 112 | secondaryQuitButtonText 113 | subHeader 114 | subHeader 115 | subHeaderUnsupported1 116 | subHeaderUnsupported 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug (-bundle-mode-json).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 79 | 80 | 86 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug (-bundle-mode-json, -simulate-os-version, -simulate-hardware-id).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 81 | 82 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug (-bundle-mode-profile).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 79 | 80 | 86 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug (-demo-mode).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 79 | 80 | 86 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug (-demo-mode, -debug-ui-mode, -force-screenshot-icon).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 81 | 82 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug (-demo-mode, -force-screenshot-icon).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug (-demo-mode, -simple-mode).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug (-simple-mode).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 79 | 80 | 86 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Debug.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 39 | 45 | 46 | 47 | 49 | 55 | 56 | 57 | 58 | 59 | 69 | 71 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 92 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcshareddata/xcschemes/Nudge - Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcuserdata/erikg.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Nudge.xcodeproj/xcuserdata/erikg.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Nudge - Debug (-bundle-mode-json).xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 4 11 | 12 | Nudge - Debug (-bundle-mode-json, -simulate-os-version, -simulate-hardware-id).xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 5 16 | 17 | Nudge - Debug (-bundle-mode-profile).xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 6 21 | 22 | Nudge - Debug (-demo-mode).xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 3 26 | 27 | Nudge - Debug (-demo-mode, -debug-ui-mode, -force-screenshot-icon).xcscheme_^#shared#^_ 28 | 29 | orderHint 30 | 2 31 | 32 | Nudge - Debug (-demo-mode, -force-screenshot-icon).xcscheme_^#shared#^_ 33 | 34 | orderHint 35 | 7 36 | 37 | Nudge - Debug (-demo-mode, -simple-mode).xcscheme_^#shared#^_ 38 | 39 | orderHint 40 | 8 41 | 42 | Nudge - Debug (-simple-mode).xcscheme_^#shared#^_ 43 | 44 | orderHint 45 | 1 46 | 47 | Nudge - Debug.xcscheme_^#shared#^_ 48 | 49 | orderHint 50 | 0 51 | 52 | Nudge - Release.xcscheme_^#shared#^_ 53 | 54 | orderHint 55 | 9 56 | 57 | Nudge Release.xcscheme_^#shared#^_ 58 | 59 | isShown 60 | 61 | orderHint 62 | 1 63 | 64 | Nudge.xcscheme_^#shared#^_ 65 | 66 | orderHint 67 | 0 68 | 69 | 70 | SuppressBuildableAutocreation 71 | 72 | 63D7D0DE25C9E9A400236281 73 | 74 | primary 75 | 76 | 77 | 63D7D0F025C9E9A500236281 78 | 79 | primary 80 | 81 | 82 | 63D7D0FB25C9E9A500236281 83 | 84 | primary 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /Nudge/3rd Party Assets/gdmf.swift: -------------------------------------------------------------------------------- 1 | // 2 | // gdmf.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 3/27/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // Define the root structure 11 | struct GDMFAssetInfo: Codable { 12 | let publicAssetSets: AssetSets 13 | let assetSets: AssetSets 14 | let publicRapidSecurityResponses: AssetSets? 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case publicAssetSets = "PublicAssetSets" 18 | case assetSets = "AssetSets" 19 | case publicRapidSecurityResponses = "PublicRapidSecurityResponses" 20 | } 21 | } 22 | 23 | // Represents both PublicAssetSets and AssetSets 24 | struct AssetSets: Codable { 25 | let iOS: [Asset]? 26 | let xrOS: [Asset]? 27 | let macOS: [Asset]? 28 | let visionOS: [Asset]? 29 | 30 | enum CodingKeys: String, CodingKey { 31 | case iOS = "iOS" 32 | case xrOS = "xrOS" 33 | case macOS = "macOS" 34 | case visionOS = "visionOS" 35 | } 36 | } 37 | 38 | // Represents an individual asset 39 | struct Asset: Codable { 40 | let productVersion: String 41 | let build: String 42 | let postingDate: String 43 | let expirationDate: String 44 | let supportedDevices: [String] 45 | 46 | enum CodingKeys: String, CodingKey { 47 | case productVersion = "ProductVersion" 48 | case build = "Build" 49 | case postingDate = "PostingDate" 50 | case expirationDate = "ExpirationDate" 51 | case supportedDevices = "SupportedDevices" 52 | } 53 | } 54 | 55 | extension GDMFAssetInfo { 56 | init(data: Data) throws { 57 | let decoder = JSONDecoder() 58 | decoder.dateDecodingStrategy = .iso8601 // Use ISO 8601 date format 59 | self = try decoder.decode(GDMFAssetInfo.self, from: data) 60 | } 61 | 62 | init(_ json: String, using encoding: String.Encoding = .utf8) throws { 63 | guard let data = json.data(using: encoding) else { 64 | throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil) 65 | } 66 | try self.init(data: data) 67 | } 68 | 69 | init(fromURL url: URL) throws { 70 | try self.init(data: try Data(contentsOf: url)) 71 | } 72 | 73 | func with( 74 | PublicAssetSets: AssetSets, 75 | AssetSets: AssetSets, 76 | PublicRapidSecurityResponses: AssetSets 77 | ) -> GDMFAssetInfo { 78 | return GDMFAssetInfo( 79 | publicAssetSets: PublicAssetSets, 80 | assetSets: AssetSets, 81 | publicRapidSecurityResponses: PublicRapidSecurityResponses 82 | ) 83 | } 84 | } 85 | 86 | // https://arvindcs.medium.com/ssl-pinning-in-ios-30ee13f3202d 87 | class GDMFPinnedSSL: NSObject { 88 | static let shared = GDMFPinnedSSL() 89 | 90 | // Create an array to store the public keys of the trusted certificates 91 | // To get these certs, download them as .cer, convert to .der, then base64 encode 92 | //// openssl x509 -in Apple\ Server\ Authentication\ CA.cer -outform der -out Apple\ Server\ Authentication\ CA.der 93 | /// base64 -i Apple\ Server\ Authentication\ CA.der 94 | let trustedCertificates: [SecCertificate] = [ 95 | // Apple Root CA 96 | SecCertificateCreateWithData(nil, Data(base64Encoded: "MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9wtj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IWq6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKMaLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAEggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBcNplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQPy3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4FgxhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oPIQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AXUKqK1drk/NAJBzewdXUh")! as CFData)!, 97 | // Apple Server Authentication CA 98 | SecCertificateCreateWithData(nil, Data(base64Encoded: "MIID+DCCAuCgAwIBAgIII2l0BK3LgxQwDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTE0MDMwODAxNTMwNFoXDTI5MDMwODAxNTMwNFowbTEnMCUGA1UEAwweQXBwbGUgU2VydmVyIEF1dGhlbnRpY2F0aW9uIENBMSAwHgYDVQQLDBdDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5Jhawy4ercRWSjt+qPuGA11O6pGDMfIVy9zB8CU9XDUr/4V7JS1ATAmSxvTk10dcEUcEY+iL6rt+YGNa/Tk1DEPoliJ/TQIV25SKBtlRFc5qL45xIGoZ6w1Hi2pX4pH3bMN5sDsTF9WyY56b6VyAdGXN6Ds1jD7cniC7hmmiCuEBsYxYkZivnsuJUfeeIOaIbgT4C0znYl3dKMgzWCgqzBJvxcm9jqBUebDfoD9tTkNYpXLxqV5tGeAo+JOqaP6HYP/XbbqhsgrXdmTjsklaUpsVzJtGuCLLGUueOdkuJuFQPbuDZQtsqZYdGFLuWuFe7UeaEE/cNobaJrHzRIXSrAgMBAAGjgaYwgaMwHQYDVR0OBBYEFCzFbVLdMe+M7AiB7d/cykMARQHQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDovL2NybC5hcHBsZS5jb20vcm9vdC5jcmwwDgYDVR0PAQH/BAQDAgEGMBAGCiqGSIb3Y2QGAgwEAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQAj8QZ+UEGBol7TcKRJka/YzGeMoSV9xJqTOS/YafsbQVtE19lryzslCRry9OPHnOiwW/Df3SIlERWTuUle2gxmel7Xb/Bj1GWMxHpUfVZPZZr92sSyyLC4oct94EeoQBW4FhntW2GO36rQzdI6wH46nyJO39/0ThrNk//Q8EVVZDM+1OXaaKATinYwJ9S/+B529vnDAO+xg+pTbVw1xw0HAbr4Ybn+xZprQ2GBA+u6X3Cd6G+UJEvczpKoLqI1PONJ4BZ3otxruY0YQrk2lkMyxst2mTU22FbGmF3Db6V+lcLVegoCIGZ4kvJnpCMN6Am9zCExEKC9vrXdTN1GA5mZ")! as CFData)! 99 | ] 100 | 101 | func pinAsynch(url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) { 102 | let request = URLRequest(url: url) 103 | let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) 104 | let task = session.dataTask(with: request, completionHandler: completion) 105 | task.resume() 106 | } 107 | 108 | func pinSync(url: URL, maxRetries: Int = 3) -> (data: Data?, response: URLResponse?, error: Error?) { 109 | let semaphore = DispatchSemaphore(value: 0) 110 | let request = URLRequest(url: url) 111 | let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) 112 | var attempts = 0 113 | 114 | var responseData: Data? 115 | var response: URLResponse? 116 | var responseError: Error? 117 | 118 | // Retry loop 119 | while attempts < maxRetries { 120 | attempts += 1 121 | let task = session.dataTask(with: request) { data, resp, error in 122 | responseData = data 123 | response = resp 124 | responseError = error 125 | semaphore.signal() 126 | } 127 | task.resume() 128 | 129 | semaphore.wait() 130 | 131 | // Break the loop if the task succeeded or return an error other than a timeout 132 | if responseError == nil || (responseError! as NSError).code != NSURLErrorTimedOut { 133 | break 134 | } else if attempts < maxRetries { 135 | // Reset the error to try again 136 | responseError = nil 137 | } 138 | } 139 | 140 | return (responseData, response, responseError) 141 | } 142 | } 143 | 144 | extension GDMFPinnedSSL: URLSessionDelegate { 145 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 146 | // Check if the certificate is trusted 147 | if let serverTrust = challenge.protectionSpace.serverTrust, 148 | SecTrustGetCertificateCount(serverTrust) > 0 { 149 | if SecTrustGetCertificateCount(serverTrust) > 1 { 150 | // Convert certificate stores to maps so they can be compared 151 | let trustedCertificatesData = trustedCertificates.map { SecCertificateCopyData($0) as Data } 152 | let serverCertificatesArray = SecTrustCopyCertificateChain(serverTrust)! as! [SecCertificate] 153 | let serverCertificatesData = serverCertificatesArray.map { SecCertificateCopyData($0) as Data } 154 | 155 | if !trustedCertificatesData.filter(serverCertificatesData.contains).isEmpty { 156 | completionHandler(.useCredential, URLCredential(trust: serverTrust)) 157 | return 158 | } 159 | } else { 160 | // Single certs we just loop through the internal nudge trust and compare if any exist 161 | let serverCertificate = SecTrustCopyCertificateChain(serverTrust)! 162 | let serverCertificateData = SecCertificateCopyData(serverCertificate as! SecCertificate) as Data 163 | 164 | for trustedCertificate in trustedCertificates { 165 | let trustedCertificateData = SecCertificateCopyData(trustedCertificate) as Data 166 | if serverCertificateData == trustedCertificateData { 167 | completionHandler(.useCredential, URLCredential(trust: serverTrust)) 168 | return 169 | } 170 | } 171 | } 172 | } 173 | // If the certificate is not trusted, cancel the request 174 | completionHandler(.cancelAuthenticationChallenge, nil) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Nudge/3rd Party Assets/sofa.swift: -------------------------------------------------------------------------------- 1 | // 2 | // sofa.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 5/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MacOSDataFeed: Codable { 11 | let updateHash: String 12 | let osVersions: [SofaOSVersion] 13 | let xProtectPayloads: XProtectPayloads 14 | let xProtectPlistConfigData: XProtectPlistConfigData 15 | let models: [String: ModelInfo] 16 | let installationApps: InstallationApps 17 | 18 | enum CodingKeys: String, CodingKey { 19 | case updateHash = "UpdateHash" 20 | case osVersions = "OSVersions" 21 | case xProtectPayloads = "XProtectPayloads" 22 | case xProtectPlistConfigData = "XProtectPlistConfigData" 23 | case models = "Models" 24 | case installationApps = "InstallationApps" 25 | } 26 | } 27 | 28 | struct SofaOSVersion: Codable { 29 | let osVersion: String 30 | let latest: LatestOS 31 | let securityReleases: [SecurityRelease] 32 | let supportedModels: [SupportedModel] 33 | 34 | enum CodingKeys: String, CodingKey { 35 | case osVersion = "OSVersion" 36 | case latest = "Latest" 37 | case securityReleases = "SecurityReleases" 38 | case supportedModels = "SupportedModels" 39 | } 40 | } 41 | 42 | protocol OSInformation { 43 | var productVersion: String { get } 44 | var build: String { get } 45 | var releaseDate: Date? { get } 46 | var securityInfo: String { get } 47 | var supportedDevices: [String] { get } 48 | var cves: [String: Bool] { get } 49 | var activelyExploitedCVEs: [String] { get } 50 | var uniqueCVEsCount: Int { get } 51 | } 52 | 53 | struct LatestOS: Codable { 54 | let productVersion, build: String 55 | let releaseDate: Date? 56 | let expirationDate: Date 57 | let supportedDevices: [String] 58 | let securityInfo: String 59 | let cves: [String: Bool] 60 | let activelyExploitedCVEs: [String] 61 | let uniqueCVEsCount: Int 62 | 63 | enum CodingKeys: String, CodingKey { 64 | case productVersion = "ProductVersion" 65 | case build = "Build" 66 | case releaseDate = "ReleaseDate" 67 | case expirationDate = "ExpirationDate" 68 | case supportedDevices = "SupportedDevices" 69 | case securityInfo = "SecurityInfo" 70 | case cves = "CVEs" 71 | case activelyExploitedCVEs = "ActivelyExploitedCVEs" 72 | case uniqueCVEsCount = "UniqueCVEsCount" 73 | } 74 | } 75 | 76 | extension LatestOS: OSInformation { 77 | // All required properties are already implemented 78 | } 79 | 80 | struct SecurityRelease: Codable { 81 | let updateName, productVersion: String 82 | let releaseDate: Date? 83 | let securityInfo: String 84 | let supportedDevices: [String] 85 | let cves: [String: Bool] 86 | let activelyExploitedCVEs: [String] 87 | let uniqueCVEsCount, daysSincePreviousRelease: Int 88 | 89 | enum CodingKeys: String, CodingKey { 90 | case updateName = "UpdateName" 91 | case productVersion = "ProductVersion" 92 | case releaseDate = "ReleaseDate" 93 | case securityInfo = "SecurityInfo" 94 | case supportedDevices = "SupportedDevices" 95 | case cves = "CVEs" 96 | case activelyExploitedCVEs = "ActivelyExploitedCVEs" 97 | case uniqueCVEsCount = "UniqueCVEsCount" 98 | case daysSincePreviousRelease = "DaysSincePreviousRelease" 99 | } 100 | } 101 | 102 | extension SecurityRelease: OSInformation { 103 | var build: String { 104 | "" 105 | } // fake out build for now 106 | } 107 | 108 | struct SupportedModel: Codable { 109 | let model: String 110 | let url: String 111 | let identifiers: [String: String] 112 | 113 | enum CodingKeys: String, CodingKey { 114 | case model = "Model" 115 | case url = "URL" 116 | case identifiers = "Identifiers" 117 | } 118 | } 119 | 120 | struct XProtectPayloads: Codable { 121 | let xProtectFramework, pluginService: String 122 | let releaseDate: Date 123 | 124 | enum CodingKeys: String, CodingKey { 125 | case xProtectFramework = "com.apple.XProtectFramework.XProtect" 126 | case pluginService = "com.apple.XprotectFramework.PluginService" 127 | case releaseDate = "ReleaseDate" 128 | } 129 | } 130 | 131 | struct XProtectPlistConfigData: Codable { 132 | let xProtect: String 133 | let releaseDate: Date 134 | 135 | enum CodingKeys: String, CodingKey { 136 | case xProtect = "com.apple.XProtect" 137 | case releaseDate = "ReleaseDate" 138 | } 139 | } 140 | 141 | struct ModelInfo: Codable { 142 | let marketingName: String 143 | let supportedOS: [String] 144 | let osVersions: [Int] 145 | 146 | enum CodingKeys: String, CodingKey { 147 | case marketingName = "MarketingName" 148 | case supportedOS = "SupportedOS" 149 | case osVersions = "OSVersions" 150 | } 151 | } 152 | 153 | struct InstallationApps: Codable { 154 | let latestUMA: UMA 155 | let allPreviousUMA: [UMA] 156 | let latestMacIPSW: MacIPSW 157 | 158 | enum CodingKeys: String, CodingKey { 159 | case latestUMA = "LatestUMA" 160 | case allPreviousUMA = "AllPreviousUMA" 161 | case latestMacIPSW = "LatestMacIPSW" 162 | } 163 | } 164 | 165 | struct UMA: Codable { 166 | let title, version, build, appleSlug, url: String 167 | 168 | enum CodingKeys: String, CodingKey { 169 | case title, version, build 170 | case appleSlug = "apple_slug" 171 | case url 172 | } 173 | } 174 | 175 | struct MacIPSW: Codable { 176 | let macosIpswURL: String 177 | let macosIpswBuild, macosIpswVersion, macosIpswAppleSlug: String 178 | 179 | enum CodingKeys: String, CodingKey { 180 | case macosIpswURL = "macos_ipsw_url" 181 | case macosIpswBuild = "macos_ipsw_build" 182 | case macosIpswVersion = "macos_ipsw_version" 183 | case macosIpswAppleSlug = "macos_ipsw_apple_slug" 184 | } 185 | } 186 | 187 | extension MacOSDataFeed { 188 | init(data: Data) throws { 189 | let decoder = JSONDecoder() 190 | decoder.dateDecodingStrategy = .iso8601 // Use ISO 8601 date format 191 | self = try decoder.decode(MacOSDataFeed.self, from: data) 192 | } 193 | 194 | init(_ json: String, using encoding: String.Encoding = .utf8) throws { 195 | guard let data = json.data(using: encoding) else { 196 | throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil) 197 | } 198 | try self.init(data: data) 199 | } 200 | 201 | init(fromURL url: URL) throws { 202 | try self.init(data: try Data(contentsOf: url)) 203 | } 204 | 205 | func with( 206 | updateHash: String, 207 | osVersions: [SofaOSVersion], 208 | xProtectPayloads: XProtectPayloads, 209 | xProtectPlistConfigData: XProtectPlistConfigData, 210 | models: [String: ModelInfo], 211 | installationApps: InstallationApps 212 | ) -> MacOSDataFeed { 213 | return MacOSDataFeed( 214 | updateHash: updateHash, 215 | osVersions: osVersions, 216 | xProtectPayloads: xProtectPayloads, 217 | xProtectPlistConfigData: xProtectPlistConfigData, 218 | models: models, 219 | installationApps: installationApps 220 | ) 221 | } 222 | } 223 | 224 | class SOFA: NSObject, URLSessionDelegate { 225 | func URLSync(url: URL, maxRetries: Int = 3, clearCache: Bool = false) -> (data: Data?, response: URLResponse?, error: Error?, responseCode: Int?, eTag: String?) { 226 | if url.scheme == "file" { 227 | do { 228 | let data = try Data(contentsOf: url) 229 | return (data, nil, nil, 200, nil) 230 | } catch { 231 | return (nil, nil, error, nil, nil) 232 | } 233 | } 234 | 235 | let semaphore = DispatchSemaphore(value: 0) 236 | var lastEtag = Globals.nudgeDefaults.string(forKey: "LastEtag") ?? "" 237 | var request = URLRequest(url: url) 238 | let config = URLSessionConfiguration.default 239 | config.requestCachePolicy = .useProtocolCachePolicy 240 | if clearCache { 241 | URLCache.shared.removeAllCachedResponses() 242 | lastEtag = "" 243 | } 244 | let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) 245 | 246 | request.addValue("\(Globals.bundleID)/\(VersionManager.getNudgeVersion())", forHTTPHeaderField: "User-Agent") 247 | request.setValue(lastEtag, forHTTPHeaderField: "If-None-Match") 248 | // TODO: I'm saving the Etag and sending it, but due to forcing this into a syncronous call, it is always returning a 200 code. When using this in an asycronous method, it eventually returns the 304 response. I'm not sure how to fix this bug. 249 | request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding") // Force compression for JSON 250 | 251 | var attempts = 0 252 | var responseData: Data? 253 | var response: URLResponse? 254 | var responseError: Error? 255 | var responseCode: Int? 256 | var eTag: String? 257 | var successfulQuery = false 258 | 259 | // Retry loop 260 | while attempts < maxRetries { 261 | attempts += 1 262 | let task = session.dataTask(with: request) { data, resp, error in 263 | guard let httpResponse = resp as? HTTPURLResponse else { 264 | LogManager.error("Error receiving response: \(error?.localizedDescription ?? "No error information")", logger: utilsLog) 265 | semaphore.signal() 266 | return 267 | } 268 | 269 | responseCode = httpResponse.statusCode 270 | response = resp 271 | responseError = error 272 | 273 | if responseCode == 200 { 274 | if let etag = httpResponse.allHeaderFields["Etag"] as? String { 275 | eTag = etag 276 | } 277 | successfulQuery = true 278 | 279 | if let encoding = httpResponse.allHeaderFields["Content-Encoding"] as? String { 280 | LogManager.debug("Content-Encoding: \(encoding)", logger: utilsLog) 281 | } 282 | 283 | responseData = data 284 | 285 | } else if responseCode == 304 { 286 | successfulQuery = true 287 | } 288 | 289 | semaphore.signal() 290 | } 291 | 292 | let timeout = DispatchWorkItem { 293 | task.cancel() 294 | semaphore.signal() 295 | } 296 | 297 | DispatchQueue.global().asyncAfter(deadline: .now() + 10, execute: timeout) 298 | task.resume() 299 | semaphore.wait() 300 | timeout.cancel() 301 | 302 | if successfulQuery { 303 | break 304 | } 305 | 306 | // Check if we should retry the request 307 | if let error = responseError as NSError? { 308 | if error.code == NSURLErrorTimedOut && attempts < maxRetries { 309 | continue // Retry only if it's a timeout error 310 | } 311 | break // Break for all other errors or no errors 312 | } 313 | } 314 | 315 | return (responseData, response, responseError, responseCode, eTag) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/CompanyScreenshotIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "GenericLogo_Normal.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "GenericLogo_Normal@2x.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 | -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/CompanyScreenshotIcon.imageset/GenericLogo_Normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/CompanyScreenshotIcon.imageset/GenericLogo_Normal.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/CompanyScreenshotIcon.imageset/GenericLogo_Normal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/Nudge/Assets.xcassets/CompanyScreenshotIcon.imageset/GenericLogo_Normal@2x.png -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Nudge/Assets.xcassets/ProductPageIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "scale" : "3x" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Nudge/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 2.0.12 19 | CFBundleVersion 20 | 2.0.12 21 | LSApplicationCategoryType 22 | public.app-category.utilities 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | LSUIElement 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Nudge/Nudge-Debug.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.disable-library-validation 8 | 9 | com.apple.security.files.user-selected.read-write 10 | 11 | com.apple.security.get-task-allow 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Nudge/Nudge.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.get-task-allow 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Nudge/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Nudge/Scripts/postinstall-launchagent: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-Present Erik Gomez. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the 'License'); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an 'AS IS' BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # If you change your agent file name, update the following line 17 | launch_agent_plist_name='com.github.macadmins.Nudge.plist' 18 | 19 | # Base paths 20 | launch_agent_base_path='/Library/LaunchAgents' 21 | 22 | # Fail the install if the admin forgets to change their paths and they don't exist. 23 | if [[ ! -e "${launch_agent_base_path}/${launch_agent_plist_name}" ]]; then 24 | echo "LaunchAgent missing, exiting" 25 | exit 1 26 | fi 27 | 28 | # Current console user information 29 | console_user=$(/usr/bin/stat -f "%Su" /dev/console) 30 | console_user_uid=$(/usr/bin/id -u "$console_user") 31 | 32 | # Only enable the LaunchAgent if there is a user logged in, otherwise rely on built in LaunchAgent behavior 33 | if [[ -z "$console_user" ]]; then 34 | echo "Did not detect user" 35 | elif [[ "$console_user" == "loginwindow" ]]; then 36 | echo "Detected Loginwindow Environment" 37 | elif [[ "$console_user" == "_mbsetupuser" ]]; then 38 | echo "Detect SetupAssistant Environment" 39 | elif [[ "$console_user" == "root" ]]; then 40 | echo "Detect root as currently logged-in user" 41 | else 42 | # Unload the agent so it can be triggered on re-install 43 | /bin/launchctl asuser "${console_user_uid}" /bin/launchctl unload -w "${launch_agent_base_path}/${launch_agent_plist_name}" 44 | # Kill Nudge just in case (say someone manually opens it and not launched via launchagent 45 | /usr/bin/killall Nudge 46 | # Load the launch agent 47 | /bin/launchctl asuser "${console_user_uid}" /bin/launchctl load -w "${launch_agent_base_path}/${launch_agent_plist_name}" 48 | fi 49 | -------------------------------------------------------------------------------- /Nudge/Scripts/postinstall-logger: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-Present Erik Gomez. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the 'License'); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an 'AS IS' BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # If you change your agent file name, update the following line 17 | launch_daemon_plist_name='com.github.macadmins.Nudge.logger.plist' 18 | 19 | # Base paths 20 | launch_daemon_base_path='/Library/LaunchDaemons' 21 | 22 | # Fail the install if the admin forgets to change their paths and they don't exist. 23 | if [[ ! -e "${launch_daemon_base_path}/${launch_daemon_plist_name}" ]]; then 24 | echo "LaunchDaemon missing, exiting" 25 | exit 1 26 | fi 27 | 28 | # Unload the agent so it can be triggered on re-install 29 | /bin/launchctl unload -w "${launch_daemon_base_path}/${launch_daemon_plist_name}" 30 | # Load the launch agent 31 | /bin/launchctl load -w "${launch_daemon_base_path}/${launch_daemon_plist_name}" 32 | -------------------------------------------------------------------------------- /Nudge/Scripts/postinstall-nudge: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021-Present Erik Gomez. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the 'License'); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an 'AS IS' BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Current console user information 17 | console_user=$(/usr/bin/stat -f "%Su" /dev/console) 18 | 19 | # Only run if there is a user logged in, otherwise do nothing 20 | if [[ -z "$console_user" ]]; then 21 | echo "Did not detect user" 22 | elif [[ "$console_user" == "loginwindow" ]]; then 23 | echo "Detected Loginwindow Environment" 24 | elif [[ "$console_user" == "_mbsetupuser" ]]; then 25 | echo "Detect SetupAssistant Environment" 26 | elif [[ "$console_user" == "root" ]]; then 27 | echo "Detect root as currently logged-in user" 28 | else 29 | # Kill Nudge is running 30 | /usr/bin/pgrep -i Nudge | /usr/bin/xargs kill 31 | fi 32 | -------------------------------------------------------------------------------- /Nudge/UI/Common/AdditionalInfoButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditionalInfoButton.swift 3 | // AdditionalInfoButton 4 | // 5 | // Created by Bart Reardon on 31/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AdditionalInfoButton: View { 11 | @EnvironmentObject var appState: AppState 12 | 13 | var body: some View { 14 | HStack { 15 | Button(action: buttonAction) { 16 | Image(systemName: "questionmark.circle") 17 | } 18 | .padding(.top, 1.0) 19 | .buttonStyle(.plain) 20 | .help("Click for additional device information".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 21 | .onHoverEffect() 22 | .sheet(isPresented: $appState.additionalInfoViewIsPresented) { 23 | DeviceInfo() 24 | } 25 | Spacer() 26 | } 27 | } 28 | 29 | private func buttonAction() { 30 | LoggerUtilities().userInitiatedDeviceInfo() 31 | appState.additionalInfoViewIsPresented = true 32 | } 33 | } 34 | 35 | #if DEBUG 36 | #Preview { 37 | AdditionalInfoButton() 38 | .environmentObject(nudgePrimaryState) 39 | .previewDisplayName("AdditionalInfoButton") 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Nudge/UI/Common/BackgroundBlur.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundBlur.swift 3 | // 4 | // Created by Bart Reardon on 23/2/2022. 5 | // 6 | 7 | import Cocoa 8 | import Foundation 9 | 10 | var loopedScreen = NSScreen() 11 | 12 | class BackgroundBlurWindow: NSWindow { 13 | override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { 14 | super.init(contentRect: contentRect, styleMask: [.fullSizeContentView], backing: .buffered, defer: flag) 15 | } 16 | } 17 | 18 | class BackgroundBlurWindowController: NSWindowController { 19 | override init(window: NSWindow?) { 20 | super.init(window: window) 21 | loadWindow() 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | override func loadWindow() { 29 | let window = BackgroundBlurWindow(contentRect: NSRect.zero, styleMask: [], backing: .buffered, defer: true) 30 | window.contentViewController = BlurViewController() 31 | window.setFrame(loopedScreen.frame, display: true) 32 | window.collectionBehavior = [.canJoinAllSpaces] 33 | self.window = window 34 | } 35 | } 36 | 37 | class BlurViewController: NSViewController { 38 | private var blurView: NSVisualEffectView? 39 | 40 | override func loadView() { 41 | view = NSView() 42 | } 43 | 44 | override func viewWillAppear() { 45 | super.viewWillAppear() 46 | setupBlurView() 47 | } 48 | 49 | override func viewWillDisappear() { 50 | super.viewWillDisappear() 51 | blurView?.removeFromSuperview() 52 | } 53 | 54 | private func setupBlurView() { 55 | guard let contentView = view.window?.contentView else { return } 56 | 57 | view.window?.isOpaque = false 58 | view.window?.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)) - 1) 59 | 60 | let blurView = NSVisualEffectView(frame: contentView.bounds) 61 | blurView.blendingMode = .behindWindow 62 | blurView.material = .fullScreenUI 63 | blurView.state = .active 64 | contentView.addSubview(blurView) 65 | self.blurView = blurView 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Nudge/UI/Common/CloseButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CloseButton.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 1/3/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct CloseButton: View { 12 | var body: some View { 13 | Image(systemName: "xmark.circle") 14 | .resizable() 15 | .frame(width: 20, height: 20) 16 | .foregroundColor(.red) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Nudge/UI/Common/CompanyLogo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompanyLogo.swift 3 | // CompanyLogo 4 | // 5 | // Created by Bart Reardon on 31/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CompanyLogo: View { 11 | @EnvironmentObject var appState: AppState 12 | @Environment(\.colorScheme) var colorScheme 13 | 14 | private var companyLogoPath: String { 15 | ImageManager().getCompanyLogoPath(colorScheme: colorScheme) 16 | } 17 | 18 | var body: some View { 19 | Group { 20 | if shouldShowCompanyLogo() { 21 | companyImage 22 | .overlay(companyImageOverlay, alignment: .topTrailing) 23 | } else if UIUtilities().showEasterEgg() { 24 | easterEggView 25 | } else { 26 | defaultImage 27 | } 28 | } 29 | } 30 | 31 | private var companyImage: some View { 32 | AsyncImage(url: UIUtilities().createCorrectURLType(from: companyLogoPath)) { phase in 33 | switch phase { 34 | case .empty: 35 | Image(systemName: "square.dashed") 36 | .customResizable(width: uiConstants.logoWidth, height: uiConstants.logoHeight) 37 | .customFontWeight(fontWeight: .ultraLight) 38 | .opacity(0.05) 39 | case .failure: 40 | Image(systemName: "questionmark.square.dashed") 41 | .customResizable(width: uiConstants.logoWidth, height: uiConstants.logoHeight) 42 | .customFontWeight(fontWeight: .ultraLight) 43 | .opacity(0.05) 44 | case .success(let image): 45 | image 46 | .customResizable(width: uiConstants.logoWidth, height: uiConstants.logoHeight) 47 | @unknown default: 48 | EmptyView() 49 | } 50 | } 51 | } 52 | 53 | private var companyImageOverlay: some View { 54 | guard !appState.deviceSupportedByOSVersion else { return AnyView(EmptyView()) } 55 | return AnyView( 56 | Image(systemName: "exclamationmark.triangle") 57 | .symbolRenderingMode(.hierarchical) 58 | .foregroundStyle(Color.red) 59 | .font(.title) 60 | ) 61 | } 62 | 63 | private var defaultImage: some View { 64 | Image(systemName: "applelogo") 65 | .customResizable(width: uiConstants.logoWidth, height: uiConstants.logoHeight) 66 | } 67 | 68 | private var easterEggView: some View { 69 | VStack(spacing: 0) { 70 | Color.green 71 | Color.yellow 72 | Color.orange 73 | Color.red 74 | Color.purple 75 | Color.blue 76 | } 77 | .frame(width: uiConstants.logoWidth, height: uiConstants.logoHeight) 78 | .mask( 79 | defaultImage 80 | ) 81 | } 82 | 83 | private func shouldShowCompanyLogo() -> Bool { 84 | ["data:", "https://", "http://", "file://"].contains(where: companyLogoPath.starts(with:)) || FileManager.default.fileExists(atPath: companyLogoPath) 85 | } 86 | } 87 | 88 | #if DEBUG 89 | #Preview { 90 | CompanyLogo() 91 | .environmentObject(nudgePrimaryState) 92 | .previewDisplayName("CompanyLogo") 93 | } 94 | #endif 95 | -------------------------------------------------------------------------------- /Nudge/UI/Common/DeferView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeferView.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 8/16/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct DeferView: View { 12 | @EnvironmentObject var appState: AppState 13 | 14 | private let edgePadding: CGFloat = 4 15 | private let horizontalPadding: CGFloat = 30 16 | private let bottomPadding: CGFloat = 10 17 | 18 | var body: some View { 19 | VStack(alignment: .center) { 20 | closeButton 21 | datePickerStack 22 | Divider() 23 | // a bit of space at the bottom to raise the Defer button away from the very edge 24 | deferButton 25 | .padding(.bottom, bottomPadding) 26 | } 27 | .background(Color(NSColor.windowBackgroundColor)) 28 | 29 | 30 | } 31 | 32 | private var closeButton: some View { 33 | HStack { 34 | Button(action: { appState.deferViewIsPresented = false }) { 35 | CloseButton() 36 | } 37 | .keyboardShortcut(.escape) 38 | .buttonStyle(.plain) 39 | .help("Click to close".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 40 | .onHoverEffect() 41 | .padding(edgePadding) 42 | Spacer() 43 | } 44 | } 45 | 46 | private var datePickerStack: some View { 47 | // We have two DatePickers because DatePicker is non-ideal 48 | VStack { 49 | DatePicker("", selection: $appState.nudgeCustomEventDate, in: limitRange) 50 | .datePickerStyle(.graphical) 51 | .labelsHidden() 52 | DatePicker("", selection: $appState.nudgeCustomEventDate, in: limitRange, displayedComponents: [.hourAndMinute]) 53 | .labelsHidden() 54 | .frame(maxWidth: 100) 55 | } 56 | .padding(.horizontal, horizontalPadding) 57 | } 58 | 59 | private var deferButton: some View { 60 | Button(action: deferAction) { 61 | Text(UserInterfaceVariables.customDeferralDropdownText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 62 | .frame(minWidth: 35) 63 | } 64 | } 65 | 66 | private func deferAction() { 67 | UIUtilities().setDeferralTime(deferralTime: appState.nudgeCustomEventDate) 68 | userHasClickedDeferralQuitButton(deferralTime: appState.nudgeCustomEventDate) 69 | appState.shouldExit = true 70 | appState.userQuitDeferrals += 1 71 | appState.userDeferrals = appState.userSessionDeferrals + appState.userQuitDeferrals 72 | LoggerUtilities().logUserSessionDeferrals() 73 | LoggerUtilities().logUserQuitDeferrals() 74 | LoggerUtilities().logUserDeferrals() 75 | UIUtilities().userInitiatedExit() 76 | } 77 | 78 | private var limitRange: ClosedRange { 79 | // Ensure the current date is consistently used throughout the calculation. 80 | let currentDate = DateManager().getCurrentDate() 81 | 82 | // Calculate the window time in days based on the UserExperienceVariables.calendarDeferralUnit. 83 | let windowTimeInDays: Int 84 | switch UserExperienceVariables.calendarDeferralUnit { 85 | case "approachingWindowTime": 86 | windowTimeInDays = UserExperienceVariables.approachingWindowTime / 24 87 | case "imminentWindowTime": 88 | windowTimeInDays = UserExperienceVariables.imminentWindowTime / 24 89 | default: 90 | windowTimeInDays = UserExperienceVariables.imminentWindowTime / 24 // Default or fallback case 91 | } 92 | 93 | // Calculate daysToAdd ensuring it's not negative. 94 | // It subtracts the windowTimeInDays from appState.daysRemaining, falling back to 0 if daysRemaining is negative. 95 | let daysToAdd = max(appState.daysRemaining - windowTimeInDays, 0) 96 | 97 | // Safely calculate the upper bound date by adding daysToAdd to the current date. 98 | guard let upperBoundDate = Calendar.current.date(byAdding: .day, value: daysToAdd, to: currentDate) else { 99 | fatalError("Could not calculate the upper bound date.") // Consider handling this more gracefully in production code. 100 | } 101 | 102 | return currentDate...upperBoundDate 103 | } 104 | } 105 | 106 | #if DEBUG 107 | #Preview { 108 | ForEach(["en", "es"], id: \.self) { id in 109 | DeferView() 110 | .environmentObject(nudgePrimaryState) 111 | .environment(\.locale, .init(identifier: id)) 112 | .previewDisplayName("DeferView (\(id))") 113 | } 114 | } 115 | #endif 116 | -------------------------------------------------------------------------------- /Nudge/UI/Common/DeviceInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceInfo.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 2/18/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct DeviceInfo: View { 12 | @EnvironmentObject var appState: AppState 13 | @Environment(\.colorScheme) var colorScheme 14 | 15 | var body: some View { 16 | VStack(alignment: .center, spacing: 7.5) { 17 | closeButton 18 | Text("Additional Device Information".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 19 | .fontWeight(.bold) 20 | infoRow(label: "Username:", value: DeviceManager().getSystemConsoleUsername()) 21 | infoRow(label: "Serial Number:", value: DeviceManager().getSerialNumber()) 22 | infoRow(label: "Architecture:", value: DeviceManager().getCPUTypeString()) 23 | infoRow(label: "Language:", value: UIConstants.languageCode) 24 | infoRow(label: "Version:", value: VersionManager.getNudgeVersion()) 25 | Spacer() // Vertically align Additional Device Information to center 26 | } 27 | .background(Color(NSColor.windowBackgroundColor)) 28 | .frame(width: 400, height: 200) 29 | } 30 | 31 | private var closeButton: some View { 32 | HStack { 33 | Button(action: { appState.additionalInfoViewIsPresented = false }) { 34 | CloseButton() 35 | } 36 | .buttonStyle(.plain) 37 | .help("Click to close".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 38 | .onHoverEffect() 39 | .frame(width: 30, height: 30) 40 | Spacer() // Horizontally align close button to left 41 | } 42 | } 43 | 44 | private func infoRow(label: String, value: String) -> some View { 45 | HStack { 46 | Text(label.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 47 | Text(value) 48 | .foregroundColor(colorScheme == .light ? .accessibleSecondaryLight : .accessibleSecondaryDark) 49 | } 50 | } 51 | } 52 | 53 | #if DEBUG 54 | #Preview { 55 | ForEach(["en", "es"], id: \.self) { id in 56 | DeviceInfo() 57 | .environmentObject(nudgePrimaryState) 58 | .environment(\.locale, .init(identifier: id)) 59 | .previewDisplayName("DeviceInfo (\(id))") 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /Nudge/UI/Common/InformationButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InformationButton.swift 3 | // InformationButton 4 | // 5 | // Created by Bart Reardon on 1/9/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct InformationButton: View { 11 | @EnvironmentObject var appState: AppState 12 | @Environment(\.colorScheme) var colorScheme 13 | 14 | var body: some View { 15 | HStack { 16 | informationButton 17 | Spacer() // Force the button to the left 18 | } 19 | } 20 | 21 | private var informationButton: some View { 22 | guard OSVersionRequirementVariables.aboutUpdateURL != "" else { return AnyView(EmptyView()) } 23 | var selectedURL = OSVersionRequirementVariables.aboutUpdateURL 24 | if OSVersionRequirementVariables.aboutUpdateURL == "sofa" && OptionalFeatureVariables.utilizeSOFAFeed { 25 | if nudgePrimaryState.sofaAboutUpdateURL.hasPrefix("https://") { 26 | selectedURL = nudgePrimaryState.sofaAboutUpdateURL 27 | } else { 28 | return AnyView(EmptyView()) 29 | } 30 | } 31 | 32 | return AnyView( 33 | Button(action: { 34 | UIUtilities().openMoreInfo(infoURL: selectedURL) 35 | }) { 36 | Text(.init(UserInterfaceVariables.informationButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)))) 37 | .foregroundColor(dynamicTextColor) 38 | } 39 | .buttonStyle(.plain) 40 | .help("Click for more information about the security update.".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 41 | .onHoverEffect() 42 | ) 43 | } 44 | 45 | private var dynamicTextColor: Color { 46 | colorScheme == .light ? Color.accessibleSecondaryLight : Color.accessibleSecondaryDark 47 | } 48 | } 49 | 50 | // Technically not information button as this is using the actionButtonTextUnsupported 51 | struct InformationButtonAsAction: View { 52 | @EnvironmentObject var appState: AppState 53 | @Environment(\.colorScheme) var colorScheme 54 | 55 | var body: some View { 56 | Button(action: { 57 | UIUtilities().openMoreInfoUnsupported() 58 | UIUtilities().postUpdateDeviceActions(userClicked: true, unSupportedUI: true) 59 | }) { 60 | Text(.init(UserInterfaceVariables.actionButtonTextUnsupported.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)))) 61 | } 62 | .help("Click for more information about replacing your device.".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 63 | } 64 | } 65 | 66 | #if DEBUG 67 | #Preview { 68 | ForEach(["en", "es"], id: \.self) { id in 69 | InformationButton() 70 | .environmentObject(nudgePrimaryState) 71 | .environment(\.locale, .init(identifier: id)) 72 | .previewDisplayName("InformationButton (\(id))") 73 | } 74 | } 75 | #endif 76 | -------------------------------------------------------------------------------- /Nudge/UI/Common/QuitButtons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuitButtons.swift 3 | // QuitButtons 4 | // 5 | // Created by Bart Reardon on 31/8/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct QuitButtons: View { 12 | @EnvironmentObject var appState: AppState 13 | 14 | var body: some View { 15 | HStack { 16 | if shouldShowSecondaryQuitButton { 17 | if UserExperienceVariables.allowLaterDeferralButton { 18 | secondaryQuitButton 19 | .frame(maxWidth:215, maxHeight: 30) 20 | } else { 21 | if appState.secondsRemaining > 3600 { 22 | secondaryQuitButton 23 | .frame(maxWidth:215, maxHeight: 30) 24 | } 25 | } 26 | Spacer() 27 | } 28 | if shouldShowPrimaryQuitButton { 29 | Spacer() 30 | primaryQuitButton 31 | .frame(maxWidth:215, maxHeight: 30) 32 | } 33 | } 34 | .sheet(isPresented: $appState.deferViewIsPresented) { 35 | DeferView() 36 | } 37 | } 38 | 39 | private var customDeferralButton: some View { 40 | Button(action: { appState.deferViewIsPresented = true }) { 41 | Text(UserInterfaceVariables.customDeferralButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 42 | } 43 | } 44 | 45 | private func deferAction(by timeInterval: TimeInterval) { 46 | appState.nudgeEventDate = DateManager().getCurrentDate().addingTimeInterval(timeInterval) 47 | UIUtilities().setDeferralTime(deferralTime: appState.nudgeEventDate) 48 | userHasClickedDeferralQuitButton(deferralTime: appState.nudgeEventDate) 49 | updateDeferralUI() 50 | } 51 | 52 | private func deferralButton(title: String, action: @escaping () -> Void) -> some View { 53 | Button(action: action) { 54 | Text(title.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 55 | } 56 | } 57 | 58 | private var deferralMenu: some View { 59 | Menu { 60 | deferralOptions 61 | } label: { 62 | Text(UserInterfaceVariables.customDeferralDropdownText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 63 | } 64 | } 65 | 66 | private var deferralOptions: some View { 67 | Group { 68 | if UserExperienceVariables.allowLaterDeferralButton { 69 | deferralButton(title: UserInterfaceVariables.primaryQuitButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)), action: standardDeferralAction) 70 | } 71 | if AppStateManager().allow1HourDeferral() { 72 | deferralButton(title: UserInterfaceVariables.oneHourDeferralButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)), action: { deferAction(by: Intervals.hourTimeInterval) }) 73 | } 74 | if AppStateManager().allow24HourDeferral() { 75 | deferralButton(title: UserInterfaceVariables.oneDayDeferralButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)), action: { deferAction(by: Intervals.dayTimeInterval) }) 76 | } 77 | if AppStateManager().allowCustomDeferral() { 78 | customDeferralButton 79 | } 80 | } 81 | } 82 | 83 | private var primaryQuitButton: some View { 84 | Group { 85 | if UserExperienceVariables.allowUserQuitDeferrals { 86 | if UserExperienceVariables.allowLaterDeferralButton { 87 | deferralMenu 88 | } else { 89 | if appState.secondsRemaining > 3600 { 90 | deferralMenu 91 | } 92 | } 93 | } else { 94 | standardQuitButton 95 | } 96 | } 97 | } 98 | 99 | private var secondaryQuitButton: some View { 100 | Button(action: secondaryQuitButtonAction) { 101 | Text(UserInterfaceVariables.secondaryQuitButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 102 | } 103 | } 104 | 105 | private func secondaryQuitButtonAction() { 106 | appState.hasClickedSecondaryQuitButton = true 107 | userHasClickedSecondaryQuitButton() 108 | } 109 | 110 | // Determines if the secondary quit button should be shown 111 | private var shouldShowSecondaryQuitButton: Bool { 112 | appState.requireDualQuitButtons && !appState.hasClickedSecondaryQuitButton 113 | } 114 | 115 | // Determines if the primary quit button should be shown 116 | private var shouldShowPrimaryQuitButton: Bool { 117 | !appState.requireDualQuitButtons || appState.hasClickedSecondaryQuitButton 118 | } 119 | 120 | private func standardDeferralAction() { 121 | appState.nudgeEventDate = DateManager().getCurrentDate() 122 | if OptionalFeatureVariables.honorCycleTimersOnExit { 123 | UIUtilities().setDeferralTime(deferralTime: appState.nudgeEventDate.addingTimeInterval(TimeInterval(nudgePrimaryState.timerCycle))) 124 | } else { 125 | UIUtilities().setDeferralTime(deferralTime: appState.nudgeEventDate) 126 | } 127 | updateDeferralUI() 128 | } 129 | 130 | private var standardQuitButton: some View { 131 | deferralButton(title: UserInterfaceVariables.primaryQuitButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)), action: standardDeferralAction) 132 | } 133 | 134 | private func updateDeferralUI() { 135 | // Update deferral UI logic 136 | appState.userQuitDeferrals += 1 137 | appState.userDeferrals = appState.userSessionDeferrals + appState.userQuitDeferrals 138 | LoggerUtilities().logUserSessionDeferrals() 139 | LoggerUtilities().logUserQuitDeferrals() 140 | LoggerUtilities().logUserDeferrals() 141 | UIUtilities().userInitiatedExit() 142 | } 143 | } 144 | 145 | #if DEBUG 146 | #Preview { 147 | ForEach(["en", "es"], id: \.self) { id in 148 | QuitButtons() 149 | .environmentObject(nudgePrimaryState) 150 | .environment(\.locale, .init(identifier: id)) 151 | .previewDisplayName("QuitButtons (\(id))") 152 | } 153 | } 154 | #endif 155 | -------------------------------------------------------------------------------- /Nudge/UI/Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 6/13/23. 6 | // 7 | 8 | import Foundation 9 | import UserNotifications 10 | import SwiftUI 11 | 12 | // State 13 | var globals = Globals() 14 | var uiConstants = UIConstants() 15 | var nudgePrimaryState = AppState() 16 | var nudgeLogState = LogState() 17 | 18 | struct Globals { 19 | static let bundle = Bundle.main 20 | static let bundleID = bundle.bundleIdentifier ?? "com.github.macadmins.Nudge" 21 | static let dnc = DistributedNotificationCenter.default() 22 | static let nc = NotificationCenter.default 23 | static let snc = NSWorkspace.shared.notificationCenter 24 | // Preferences 25 | static let configJSON = ConfigurationManager().getConfigurationAsJSON() 26 | static let configProfile = ConfigurationManager().getConfigurationAsProfile() 27 | static let nudgeDefaults = UserDefaults.standard 28 | static let nudgeJSONPreferences = NetworkFileManager().getNudgeJSONPreferences() 29 | // Device Properties 30 | static let gdmfAssets = NetworkFileManager().getGDMFAssets() 31 | static var sofaAssets: MacOSDataFeed? 32 | static let hardwareModelIDs = DeviceManager().getHardwareModelIDs() 33 | } 34 | 35 | struct Intervals { 36 | static let dayTimeInterval: CGFloat = 86400 37 | static let hourTimeInterval: CGFloat = 3600 38 | // Setup the main refresh timer that controls the child refresh logic 39 | static let nudgeRefreshCycleTimer = Timer.publish(every: Double(UserExperienceVariables.nudgeRefreshCycle), on: .main, in: .common).autoconnect() 40 | } 41 | 42 | struct UIConstants { 43 | static let bottomPadding: CGFloat = 10 44 | static let buttonTextMinWidth: CGFloat = 35 45 | static let contentWidthPadding: CGFloat = 25 46 | static let DNDServer = Bundle(path: "/System/Library/PrivateFrameworks/DoNotDisturbServer.framework")?.load() ?? false // Idea from https://github.com/saagarjha/vers/blob/d9460f6e14311e0a90c4c171975c93419481586b/vers/Headers.swift 47 | static let languageCode = NSLocale.current.languageCode! 48 | static let languageID = Locale.current.identifier 49 | static let leftSideWidth: CGFloat = 300 50 | static let screens = NSScreen.screens 51 | static let screenshotMaxHeight: CGFloat = 120 52 | static let screenshotTopPadding: CGFloat = 28 53 | static let serialNumber = DeviceManager().getSerialNumber() 54 | static let windowDelegate = WindowDelegate() 55 | 56 | var declaredWindowHeight: CGFloat = 450 57 | var declaredWindowWidth: CGFloat = 900 58 | var demoModeArgumentPassed = false 59 | var isPreview: Bool { 60 | return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" // https://zacwhite.com/2020/detecting-swiftui-previews/ 61 | } 62 | var logoHeight: CGFloat = 150 63 | var logoWidth: CGFloat = 200 64 | var unitTestingArgumentPassed = false 65 | } 66 | 67 | class AppState: ObservableObject { 68 | @Published var activelyExploitedCVEs = false 69 | @Published var afterFirstStateChange = false 70 | @Published var allowButtons = true 71 | @Published var daysRemaining = DateManager().getNumberOfDaysBetween() 72 | @Published var deferralCountPastThreshold = false 73 | @Published var deferRunUntil = Globals.nudgeDefaults.object(forKey: "deferRunUntil") as? Date 74 | @Published var deviceSupportedByOSVersion = true 75 | @Published var hasClickedSecondaryQuitButton = false 76 | @Published var hasLoggedDeferralCountPastThreshold = false 77 | @Published var hasLoggedDeferralCountPastThresholdDualQuitButtons = false 78 | @Published var hasLoggedRequireDualQuitButtons = false 79 | @Published var hasRenderedApplicationTerminatedNotificationImagePath = false 80 | @Published var hoursRemaining = DateManager().getNumberOfHoursRemaining() 81 | @Published var secondsRemaining = DateManager().getNumberOfSecondsRemaining() 82 | @Published var lastRefreshTime = DateManager().getFormattedDate() 83 | @Published var requireDualQuitButtons = false 84 | @Published var requiredMinimumOSVersion = OSVersionRequirementVariables.requiredMinimumOSVersion 85 | @Published var shouldExit = false 86 | @Published var sofaAboutUpdateURL: String = "" 87 | @Published var timerCycle = 0 88 | @Published var userDeferrals = Globals.nudgeDefaults.object(forKey: "userDeferrals") as? Int ?? 0 89 | @Published var userQuitDeferrals = Globals.nudgeDefaults.object(forKey: "userQuitDeferrals") as? Int ?? 0 90 | @Published var userRequiredMinimumOSVersion = Globals.nudgeDefaults.object(forKey: "requiredMinimumOSVersion") as? String ?? "0.0" 91 | @Published var userSessionDeferrals = Globals.nudgeDefaults.object(forKey: "userSessionDeferrals") as? Int ?? 0 92 | @Published var backgroundBlur = [BackgroundBlurWindowController]() 93 | @Published var screenCurrentlyLocked = false 94 | @Published var locale = Locale.current 95 | @Published var nudgeCustomEventDate = DateManager().getCurrentDate() 96 | @Published var nudgeEventDate = DateManager().getCurrentDate() 97 | @Published var screenShotZoomViewIsPresented = false 98 | @Published var deferViewIsPresented = false 99 | @Published var additionalInfoViewIsPresented = false 100 | @Published var differentiateWithoutColor = NSWorkspace.shared.accessibilityDisplayShouldDifferentiateWithoutColor 101 | } 102 | 103 | class DNDConfig { 104 | static let rawType = NSClassFromString("DNDSAuxiliaryStateMonitor") as? NSObject.Type 105 | let rawValue: NSObject? 106 | 107 | init() { 108 | if let rawType = Self.rawType { 109 | self.rawValue = rawType.init() 110 | } else { 111 | self.rawValue = nil 112 | LogManager.error("DNDSAuxiliaryStateMonitor class could not be found.", logger: utilsLog) 113 | } 114 | } 115 | 116 | required init?(rawValue: NSObject?) { 117 | guard let rawType = Self.rawType, let unwrappedRawValue = rawValue, unwrappedRawValue.isKind(of: rawType) else { 118 | LogManager.error("Initialization with rawValue failed.", logger: utilsLog) 119 | return nil 120 | } 121 | self.rawValue = unwrappedRawValue 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Nudge/UI/SimpleMode/SimpleMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleMode.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 2/2/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // SimpleMode 12 | struct SimpleMode: View { 13 | @EnvironmentObject var appState: AppState 14 | @Environment(\.colorScheme) var colorScheme 15 | 16 | var body: some View { 17 | VStack { 18 | AdditionalInfoButton().padding(3) // (?) button 19 | 20 | mainContent 21 | .frame(alignment: .center) 22 | 23 | bottomButtons 24 | } 25 | } 26 | 27 | private var mainContent: some View { 28 | VStack(alignment: .center, spacing: 10) { 29 | Spacer() 30 | CompanyLogo() 31 | Spacer() 32 | 33 | Text(appState.deviceSupportedByOSVersion ? getMainHeader().localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)) : getMainHeaderUnsupported().localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 34 | .font(.title) 35 | 36 | remainingTimeView 37 | 38 | if UserInterfaceVariables.showDeferralCount { 39 | deferralCountView 40 | } 41 | 42 | Spacer() 43 | if appState.deviceSupportedByOSVersion { 44 | Button(action: { 45 | UIUtilities().updateDevice() 46 | }) { 47 | Text(UserInterfaceVariables.actionButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 48 | .frame(minWidth: 120) 49 | } 50 | .keyboardShortcut(.defaultAction) 51 | } else { 52 | InformationButtonAsAction() 53 | } 54 | Spacer() 55 | } 56 | } 57 | 58 | private var remainingTimeView: some View { 59 | HStack(spacing: 3.5) { 60 | if (appState.daysRemaining > 0 && !CommandLineUtilities().demoModeEnabled()) || CommandLineUtilities().demoModeEnabled() { 61 | Text("Days Remaining To Update:".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 62 | Text(String(appState.daysRemaining)) 63 | .foregroundColor(colorScheme == .light ? .accessibleSecondaryLight : .accessibleSecondaryDark) 64 | } else if appState.daysRemaining == 0 && !CommandLineUtilities().demoModeEnabled() { 65 | Text("Hours Remaining To Update:".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 66 | Text(String(appState.hoursRemaining)) 67 | .foregroundColor(appState.differentiateWithoutColor ? .accessibleRed : .red) 68 | .fontWeight(.bold) 69 | } else { 70 | Text("Days Remaining To Update:".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 71 | Text(String(appState.daysRemaining)) 72 | .foregroundColor(appState.differentiateWithoutColor ? .accessibleRed : .red) 73 | .fontWeight(.bold) 74 | 75 | } 76 | } 77 | } 78 | 79 | private var deferralCountView: some View { 80 | HStack { 81 | Text("Deferred Count:".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 82 | .font(.title2) 83 | Text(String(appState.userDeferrals)) 84 | .foregroundColor(infoTextColor) 85 | .font(.title2) 86 | } 87 | } 88 | 89 | private var bottomButtons: some View { 90 | HStack { 91 | InformationButton() 92 | 93 | if appState.allowButtons || CommandLineUtilities().demoModeEnabled() { 94 | QuitButtons() 95 | } 96 | } 97 | .padding([.bottom, .leading, .trailing], UIConstants.contentWidthPadding) 98 | } 99 | 100 | private var infoTextColor: Color { 101 | colorScheme == .light ? .accessibleSecondaryLight : .accessibleSecondaryDark 102 | } 103 | } 104 | 105 | #if DEBUG 106 | #Preview { 107 | ForEach(["en", "es"], id: \.self) { id in 108 | SimpleMode() 109 | .environmentObject(nudgePrimaryState) 110 | .previewLayout(.fixed(width: uiConstants.declaredWindowWidth, height: uiConstants.declaredWindowHeight)) 111 | .environment(\.locale, .init(identifier: id)) 112 | .previewDisplayName("SimpleMode (\(id))") 113 | } 114 | } 115 | #endif 116 | -------------------------------------------------------------------------------- /Nudge/UI/StandardMode/LeftSide.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeftSide.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 2/18/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // StandardModeLeftSide 12 | struct StandardModeLeftSide: View { 13 | @EnvironmentObject var appState: AppState 14 | @Environment(\.colorScheme) var colorScheme 15 | 16 | private let bottomPadding: CGFloat = 10 17 | private let interItemSpacing: CGFloat = 20 18 | private let interLineSpacing: CGFloat = 10 19 | 20 | var body: some View { 21 | VStack { 22 | contentStack 23 | Spacer() // Force buttons to the bottom 24 | } 25 | .padding(.bottom, bottomPadding) 26 | } 27 | 28 | private var contentStack: some View { 29 | VStack(alignment: .center, spacing: interItemSpacing) { 30 | AdditionalInfoButton().padding(3) // (?) button 31 | CompanyLogo() 32 | Divider().padding(.horizontal, UIConstants.contentWidthPadding) 33 | informationStack 34 | } 35 | } 36 | 37 | private var informationStack: some View { 38 | VStack(alignment: .center, spacing: interLineSpacing) { 39 | InfoRow(label: "Required OS Version:", value: String(appState.requiredMinimumOSVersion), boldText: true) 40 | if UserInterfaceVariables.showRequiredDate { 41 | InfoRow(label: "Required Date:", value: DateManager().coerceDateToString(date: requiredInstallationDate, formatterString: UserInterfaceVariables.requiredInstallationDisplayFormat)) 42 | } 43 | if OptionalFeatureVariables.utilizeSOFAFeed && UserInterfaceVariables.showActivelyExploitedCVEs { 44 | InfoRow(label: "Actively Exploited CVEs:", value: String(appState.activelyExploitedCVEs).capitalized, isHighlighted: appState.activelyExploitedCVEs ? true : false, boldText: appState.activelyExploitedCVEs) 45 | } 46 | InfoRow(label: "Current OS Version:", value: GlobalVariables.currentOSVersion) 47 | if UserInterfaceVariables.showDaysRemainingToUpdate { 48 | remainingTimeRow 49 | } 50 | if UserInterfaceVariables.showDeferralCount { 51 | InfoRow(label: "Deferred Count:", value: String(appState.userDeferrals)) 52 | } 53 | } 54 | .padding(.horizontal, UIConstants.contentWidthPadding) 55 | } 56 | 57 | private var remainingTimeRow: some View { 58 | Group { 59 | if shouldshowDaysRemainingToUpdate { 60 | InfoRow(label: "Days Remaining To Update:", value: String(appState.daysRemaining), isHighlighted: 0 > appState.daysRemaining ? true : false) 61 | } else { 62 | InfoRow(label: "Hours Remaining To Update:", value: String(appState.hoursRemaining), isHighlighted: true) 63 | } 64 | } 65 | } 66 | 67 | private var shouldshowDaysRemainingToUpdate: Bool { 68 | ((appState.daysRemaining > 0 || 0 > appState.hoursRemaining) && !CommandLineUtilities().demoModeEnabled()) || CommandLineUtilities().demoModeEnabled() 69 | } 70 | } 71 | 72 | struct InfoRow: View { 73 | let label: String 74 | let value: String 75 | var isHighlighted: Bool = false 76 | var boldText: Bool = false 77 | 78 | @EnvironmentObject var appState: AppState 79 | @Environment(\.colorScheme) var colorScheme 80 | 81 | var body: some View { 82 | HStack { 83 | Text(label.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 84 | .fontWeight(boldText ? .bold : .regular) 85 | .lineLimit(2) 86 | Spacer() 87 | if isHighlighted { 88 | Text(value.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 89 | .foregroundColor(appState.differentiateWithoutColor ? .accessibleRed : .red) 90 | .fontWeight(.bold) 91 | .minimumScaleFactor(0.01) 92 | .lineLimit(1) 93 | } else { 94 | Text(value.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 95 | .foregroundColor(colorScheme == .light ? .accessibleSecondaryLight : .accessibleSecondaryDark) 96 | .fontWeight(boldText ? .bold : .regular) 97 | .minimumScaleFactor(0.01) 98 | .lineLimit(1) 99 | } 100 | } 101 | } 102 | } 103 | 104 | #if DEBUG 105 | #Preview { 106 | ForEach(["en", "es"], id: \.self) { id in 107 | StandardModeLeftSide() 108 | .environmentObject(nudgePrimaryState) 109 | .environment(\.locale, .init(identifier: id)) 110 | .previewDisplayName("LeftSide (\(id))") 111 | } 112 | } 113 | #endif 114 | -------------------------------------------------------------------------------- /Nudge/UI/StandardMode/RightSide.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RightSide.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 2/18/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // StandardModeRightSide 12 | struct StandardModeRightSide: View { 13 | @EnvironmentObject var appState: AppState 14 | @Environment(\.colorScheme) var colorScheme 15 | 16 | private var screenShotPath: String { 17 | ImageManager().getScreenShotPath(colorScheme: colorScheme) 18 | } 19 | 20 | var body: some View { 21 | VStack { 22 | Spacer() 23 | headerSection 24 | .padding([.leading, .trailing], UIConstants.contentWidthPadding) 25 | .padding(.bottom, UIConstants.bottomPadding) 26 | informationSection 27 | .background(Color.secondary.opacity(0.1)) 28 | .cornerRadius(5) 29 | Spacer() 30 | } 31 | .padding(.top, UIConstants.screenshotTopPadding) 32 | .padding(.bottom, UIConstants.bottomPadding) 33 | .padding([.leading, .trailing], UIConstants.contentWidthPadding) 34 | } 35 | 36 | private var headerSection: some View { 37 | VStack(alignment: .center) { 38 | HStack { 39 | VStack(alignment: .leading, spacing: 5) { 40 | HStack { 41 | Text(.init(appState.deviceSupportedByOSVersion ? getMainHeader().localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)) : getMainHeaderUnsupported().localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)))) 42 | .font(.largeTitle) 43 | .minimumScaleFactor(0.5) 44 | .frame(maxHeight: 25) 45 | .lineLimit(1) 46 | } 47 | 48 | HStack { 49 | Text(.init(appState.deviceSupportedByOSVersion ? UserInterfaceVariables.subHeader.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)) : UserInterfaceVariables.subHeaderUnsupported.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)))) 50 | .font(.body) 51 | .lineLimit(1) 52 | } 53 | } 54 | Spacer() 55 | } 56 | } 57 | } 58 | 59 | private var informationSection: some View { 60 | VStack { 61 | Spacer() 62 | .frame(height: 10) 63 | HStack(alignment: .center) { 64 | VStack(alignment: .leading, spacing: 1) { 65 | HStack { 66 | Text(.init(appState.deviceSupportedByOSVersion ? UserInterfaceVariables.mainContentHeader.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)) : UserInterfaceVariables.mainContentHeaderUnsupported.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)))) 67 | .font(.callout) 68 | Spacer() 69 | } 70 | HStack { 71 | Text(appState.deviceSupportedByOSVersion ? UserInterfaceVariables.mainContentSubHeader.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)) : UserInterfaceVariables.mainContentSubHeaderUnsupported.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 72 | .font(.callout) 73 | Spacer() 74 | } 75 | } 76 | Spacer() 77 | 78 | if appState.deviceSupportedByOSVersion { 79 | Button(action: { 80 | UIUtilities().updateDevice() 81 | }) { 82 | Text(.init(UserInterfaceVariables.actionButtonText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)))) 83 | } 84 | .keyboardShortcut(.defaultAction) 85 | } else { 86 | InformationButtonAsAction() 87 | } 88 | } 89 | 90 | Divider() 91 | 92 | HStack { 93 | Text(.init(appState.deviceSupportedByOSVersion ? UserInterfaceVariables.mainContentNote.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)) : UserInterfaceVariables.mainContentNoteUnsupported.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)))) 94 | .font(.callout) 95 | .foregroundColor(appState.differentiateWithoutColor ? .accessibleRed : .red) 96 | Spacer() 97 | } 98 | 99 | ScrollView(.vertical) { 100 | VStack { 101 | HStack { 102 | Text(.init(appState.deviceSupportedByOSVersion ? UserInterfaceVariables.mainContentText.replacingOccurrences(of: "\\n", with: "\n").localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)) : UserInterfaceVariables.mainContentTextUnsupported.replacingOccurrences(of: "\\n", with: "\n").localized(desiredLanguage: getDesiredLanguage(locale: appState.locale)))) 103 | .font(.callout) 104 | .multilineTextAlignment(.leading) 105 | Spacer() 106 | } 107 | } 108 | } 109 | screenshotDisplay 110 | } 111 | .padding([.leading, .trailing], UIConstants.contentWidthPadding) 112 | } 113 | 114 | private var screenshotDisplay: some View { 115 | Group { 116 | if shouldShowScreenshot() { 117 | screenshotButton 118 | } else { 119 | EmptyView() 120 | } 121 | } 122 | } 123 | 124 | private var screenshotButton: some View { 125 | Button(action: { appState.screenShotZoomViewIsPresented = true }) { 126 | AsyncImage(url: UIUtilities().createCorrectURLType(from: screenShotPath)) { phase in 127 | switch phase { 128 | case .empty: 129 | Image(systemName: "square.dashed") 130 | .customResizable(maxHeight: UIConstants.screenshotMaxHeight) 131 | .customFontWeight(fontWeight: .ultraLight) 132 | .opacity(0.05) 133 | case .failure: 134 | Image(systemName: "questionmark.square.dashed") 135 | .customResizable(maxHeight: UIConstants.screenshotMaxHeight) 136 | .customFontWeight(fontWeight: .ultraLight) 137 | .opacity(0.05) 138 | case .success(let image): 139 | image 140 | .customResizable(maxHeight: UIConstants.screenshotMaxHeight) 141 | @unknown default: 142 | EmptyView() 143 | } 144 | } 145 | } 146 | .buttonStyle(.plain) 147 | .help(UserInterfaceVariables.screenShotAltText.localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 148 | .sheet(isPresented: $appState.screenShotZoomViewIsPresented) { 149 | ScreenShotZoom() 150 | } 151 | .onHoverEffect() 152 | } 153 | 154 | private func shouldShowScreenshot() -> Bool { 155 | ["data:", "https://", "http://", "file://"].contains(where: screenShotPath.starts(with:)) || FileManager.default.fileExists(atPath: screenShotPath) || forceScreenShotIconMode() 156 | } 157 | } 158 | 159 | #if DEBUG 160 | #Preview { 161 | ForEach(["en", "es"], id: \.self) { id in 162 | StandardModeRightSide() 163 | .environmentObject(nudgePrimaryState) 164 | .environment(\.locale, .init(identifier: id)) 165 | .previewDisplayName("RightSide (\(id))") 166 | } 167 | } 168 | #endif 169 | -------------------------------------------------------------------------------- /Nudge/UI/StandardMode/ScreenShotZoom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenShotZoomSheet.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 2/18/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // Sheet view for Screenshot zoom 12 | struct ScreenShotZoom: View { 13 | @EnvironmentObject var appState: AppState 14 | @Environment(\.colorScheme) var colorScheme 15 | 16 | private var screenShotPath: String { 17 | ImageManager().getScreenShotPath(colorScheme: colorScheme) 18 | } 19 | 20 | var body: some View { 21 | VStack(alignment: .center) { 22 | closeButton 23 | screenShotButton 24 | Spacer() // Vertically align Screenshot to center 25 | } 26 | .background(Color(NSColor.windowBackgroundColor)) 27 | .frame(minWidth: 850, maxWidth: 1200, minHeight: 900, maxHeight: 1200) 28 | } 29 | 30 | private var closeButton: some View { 31 | HStack { 32 | Button(action: { appState.screenShotZoomViewIsPresented = false }) { 33 | CloseButton() 34 | } 35 | .buttonStyle(.plain) 36 | .help("Click to close".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 37 | .onHoverEffect() 38 | .frame(width: 30, height: 30) 39 | Spacer() // Horizontally align close button to left 40 | } 41 | } 42 | 43 | private var screenShotButton: some View { 44 | HStack { 45 | Button(action: { appState.screenShotZoomViewIsPresented = false }) { 46 | screenShotImage 47 | } 48 | .buttonStyle(.plain) 49 | .help("Click to close".localized(desiredLanguage: getDesiredLanguage(locale: appState.locale))) 50 | .onHoverEffect() 51 | } 52 | } 53 | 54 | private var screenShotImage: some View { 55 | AsyncImage(url: UIUtilities().createCorrectURLType(from: screenShotPath)) { phase in 56 | switch phase { 57 | case .empty: 58 | Image(systemName: "square.dashed") 59 | .customResizable(maxHeight: 675) 60 | .customFontWeight(fontWeight: .ultraLight) 61 | .opacity(0.05) 62 | case .failure: 63 | Image(systemName: "questionmark.square.dashed") 64 | .customResizable(maxHeight: 675) 65 | .customFontWeight(fontWeight: .ultraLight) 66 | .opacity(0.05) 67 | case .success(let image): 68 | image 69 | .customResizable() 70 | @unknown default: 71 | EmptyView() 72 | } 73 | } 74 | } 75 | } 76 | 77 | #if DEBUG 78 | #Preview { 79 | ForEach(["en", "es"], id: \.self) { id in 80 | ScreenShotZoom() 81 | .environmentObject(nudgePrimaryState) 82 | .environment(\.locale, .init(identifier: id)) 83 | .previewDisplayName("ScreenShotZoom (\(id))") 84 | } 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Nudge/UI/StandardMode/StandardMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StandardMode.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 2/2/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct StandardMode: View { 12 | @EnvironmentObject var appState: AppState 13 | 14 | var body: some View { 15 | VStack { 16 | standardModeContent 17 | bottomButtons 18 | } 19 | } 20 | 21 | private var standardModeContent: some View { 22 | HStack { 23 | StandardModeLeftSide() 24 | .frame(width: UIConstants.leftSideWidth) 25 | 26 | Divider() 27 | .padding(.vertical, UIConstants.contentWidthPadding) 28 | 29 | StandardModeRightSide() 30 | } 31 | } 32 | 33 | private var bottomButtons: some View { 34 | HStack { 35 | InformationButton() 36 | 37 | if appState.allowButtons || CommandLineUtilities().demoModeEnabled() { 38 | QuitButtons() 39 | } 40 | } 41 | .padding(.bottom, UIConstants.bottomPadding) 42 | .padding(.leading, UIConstants.contentWidthPadding) 43 | .padding(.trailing, UIConstants.contentWidthPadding) 44 | } 45 | } 46 | 47 | #if DEBUG 48 | #Preview { 49 | ForEach(["en", "es"], id: \.self) { id in 50 | StandardMode() 51 | .environmentObject(nudgePrimaryState) 52 | .previewLayout(.fixed(width: uiConstants.declaredWindowWidth, height: uiConstants.declaredWindowHeight)) 53 | .environment(\.locale, .init(identifier: id)) 54 | .previewDisplayName("StandardMode (\(id))") 55 | } 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /Nudge/Utilities/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 6/13/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // Color Extension 12 | extension Color { 13 | static let accessibleBlue = Color(red: 26 / 255, green: 133 / 255, blue: 255 / 255) 14 | static let accessibleRed = Color(red: 230 / 255, green: 97 / 255, blue: 0) 15 | static let accessibleSecondaryLight = Color(red: 100 / 255, green: 100 / 255, blue: 100 / 255) 16 | static let accessibleSecondaryDark = Color(red: 150 / 255, green: 150 / 255, blue: 150 / 255) 17 | } 18 | 19 | // Date Extension 20 | extension Date { 21 | private static var dateFormatter = DateFormatter() // Reuse DateFormatter 22 | 23 | func getFormattedDate(format: String) -> String { 24 | Date.dateFormatter.dateFormat = format 25 | return Date.dateFormatter.string(from: self) 26 | } 27 | } 28 | 29 | // FixedWidthInteger Extension 30 | extension FixedWidthInteger { 31 | // https://stackoverflow.com/a/63539782 32 | /// Calculates the number of bytes used to represent the integer. 33 | var byteWidth: Int { 34 | return self.bitWidth / UInt8.bitWidth 35 | } 36 | 37 | /// Static property to calculate the byte width of the integer type. 38 | static var byteWidth: Int { 39 | return Self.bitWidth / UInt8.bitWidth 40 | } 41 | } 42 | 43 | // Image Extension 44 | extension Image { 45 | func customResizable(width: CGFloat? = nil, height: CGFloat? = nil, minHeight: CGFloat? = nil, minWidth: CGFloat? = nil, maxHeight: CGFloat? = nil, maxWidth: CGFloat? = nil) -> some View { 46 | self 47 | .resizable() 48 | .scaledToFit() 49 | .frame(width: width, height: height, alignment: .center) 50 | .frame(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) 51 | } 52 | } 53 | 54 | extension View { 55 | @ViewBuilder 56 | func customFontWeight(fontWeight: Font.Weight? = nil) -> some View { 57 | if #available(macOS 13.0, *), let weight = fontWeight { 58 | self.fontWeight(weight) 59 | } else { 60 | self 61 | } 62 | } 63 | } 64 | 65 | // NSWorkspace Extension 66 | // Originally from // https://github.com/brackeen/calculate-widget/blob/master/Calculate/NSWindow%2BMoveToActiveSpace.swift#L64 67 | extension NSWorkspace { 68 | func isActiveSpaceFullScreen() -> Bool { 69 | guard let winInfoArray = CGWindowListCopyWindowInfo([.excludeDesktopElements, .optionOnScreenOnly], kCGNullWindowID) as? [[String: Any]] else { 70 | return false 71 | } 72 | for winInfo in winInfoArray { 73 | guard let windowLayer = winInfo[kCGWindowLayer as String] as? NSNumber, windowLayer == 0, 74 | let boundsDict = winInfo[kCGWindowBounds as String] as? [String: Any], 75 | let bounds = CGRect(dictionaryRepresentation: boundsDict as CFDictionary), 76 | bounds.size == NSScreen.main?.frame.size else { 77 | continue 78 | } 79 | return true 80 | } 81 | return false 82 | } 83 | } 84 | 85 | // Scene Extension 86 | extension Scene { 87 | func windowResizabilityContentSize() -> some Scene { 88 | // No changes needed, well implemented for macOS 13.0+ 89 | if #available(macOS 13.0, *) { 90 | return windowResizability(.contentSize) 91 | } else { 92 | return self 93 | } 94 | } 95 | } 96 | 97 | // Localization Extension 98 | extension String { 99 | func localized(desiredLanguage: String) -> String { 100 | // https://stackoverflow.com/questions/29985614/how-can-i-change-locale-programmatically-with-swift 101 | // Apple recommends against this, but this is super frustrating since Nudge does dynamic UIs 102 | let path = Bundle.main.path(forResource: desiredLanguage, ofType: "lproj") ?? 103 | Bundle.main.path(forResource: "en", ofType: "lproj") // Fallback to English 104 | 105 | guard let bundle = (path != nil) ? Bundle(path: path!) : nil else { 106 | return self // If both desired and fallback fail, return the original string 107 | } 108 | 109 | return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "") 110 | } 111 | } 112 | 113 | // View Extension 114 | extension View { 115 | func onHoverEffect() -> some View { 116 | self.onHover { inside in 117 | if inside { 118 | NSCursor.pointingHand.push() 119 | } else { 120 | NSCursor.pop() 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Nudge/Utilities/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // Nudge 4 | // 5 | // Created by Rory Murdock on 2/10/21. 6 | // 7 | 8 | import Foundation 9 | import os 10 | 11 | // Logger Manager 12 | struct LogManager { 13 | static private let bundleID = Globals.bundleID 14 | 15 | static func createLogger(category: String) -> Logger { 16 | return Logger(subsystem: bundleID, category: category) 17 | } 18 | 19 | static func debug(_ message: String, logger: Logger) { 20 | logger.debug("\(message, privacy: .public)") 21 | } 22 | 23 | static func error(_ message: String, logger: Logger) { 24 | logger.error("\(message, privacy: .public)") 25 | } 26 | 27 | static func info(_ message: String, logger: Logger) { 28 | logger.info("\(message, privacy: .public)") 29 | } 30 | 31 | static func notice(_ message: String, logger: Logger) { 32 | logger.notice("\(message, privacy: .public)") 33 | } 34 | 35 | static func warning(_ message: String, logger: Logger) { 36 | logger.warning("\(message, privacy: .public)") 37 | } 38 | } 39 | 40 | // Usage of Logger Manager 41 | let utilsLog = LogManager.createLogger(category: "utilities") 42 | let osLog = LogManager.createLogger(category: "operating-system") 43 | let loggingLog = LogManager.createLogger(category: "logging") 44 | let prefsProfileLog = LogManager.createLogger(category: "preferences-profile") 45 | let prefsJSONLog = LogManager.createLogger(category: "preferences-json") 46 | let uiLog = LogManager.createLogger(category: "user-interface") 47 | let softwareupdateDeviceLog = LogManager.createLogger(category: "softwareupdate-device") 48 | let softwareupdateListLog = LogManager.createLogger(category: "softwareupdate-list") 49 | let softwareupdateDownloadLog = LogManager.createLogger(category: "softwareupdate-download") 50 | let sofaLog = LogManager.createLogger(category: "sofa") 51 | 52 | // Log State 53 | class LogState { 54 | var afterFirstLaunch = false 55 | var afterFirstRun = false 56 | var hasLoggedBundleMode = false 57 | var hasLoggedDemoMode = false 58 | var hasLoggedMajorOSVersion = false 59 | var hasLoggedMajorRequiredOSVersion = false 60 | var hasLoggedPastRequiredInstallationDate = false 61 | var hasLoggedRequireMajorUgprade = false 62 | var hasLoggedScreenshotIconMode = false 63 | var hasLoggedSimpleMode = false 64 | var hasLoggedSimulatedDate = false 65 | var hasLoggedUnitTestingMode = false 66 | } 67 | 68 | // NudgeLogger 69 | class NudgeLogger { 70 | init() { 71 | LogManager.debug("Starting log events", logger: loggingLog) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Nudge/Utilities/OSVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSVersion.swift 3 | // Nudge 4 | // 5 | // Created by Erik Gomez on 2/8/21. 6 | // 7 | 8 | import Foundation 9 | 10 | // Version of a macOS release. Example: 11.2 11 | public struct OSVersion { 12 | public var major: Int 13 | public var minor: Int 14 | public var patch: Int 15 | 16 | /// Errors that can occur when parsing a version string. 17 | public enum ParseError: Error { 18 | case badFormat(reason: String) 19 | } 20 | 21 | /// Creates an `OSVersion` by providing all the parts. 22 | public init(major: Int, minor: Int, patch: Int) { 23 | self.major = major 24 | self.minor = minor 25 | self.patch = patch 26 | } 27 | 28 | /// Creates an `OSVersion` by converting the built-in `OperatingSystemVersion`. 29 | public init(_ version: OperatingSystemVersion) { 30 | self.major = version.majorVersion 31 | self.minor = version.minorVersion 32 | self.patch = version.patchVersion 33 | } 34 | 35 | /// Creates an `OSVersion` by parsing a string like "11.2". 36 | public init(_ string: String) throws { 37 | let parts = string.split(separator: ".", omittingEmptySubsequences: false) 38 | guard parts.count == 2 || parts.count == 3 else { 39 | let error = "Input \(string) must have 2 or 3 parts, got \(parts.count)." 40 | LogManager.error(error, logger: utilsLog) 41 | throw ParseError.badFormat(reason: error) 42 | } 43 | 44 | guard let major = Int(parts[0]), let minor = Int(parts[1]) else { 45 | let error = "Invalid format for major or minor version in \(string)." 46 | LogManager.error(error, logger: utilsLog) 47 | throw ParseError.badFormat(reason: error) 48 | } 49 | let patch = parts.count >= 3 ? Int(parts[2]) ?? 0 : 0 50 | 51 | self.init(major: major, minor: minor, patch: patch) 52 | } 53 | } 54 | 55 | extension OSVersion: CustomStringConvertible { 56 | public var description: String { 57 | "\(major).\(minor)\(patch > 0 ? ".\(patch)" : "")" 58 | } 59 | } 60 | 61 | extension OSVersion: Equatable, Comparable { 62 | public static func == (lhs: OSVersion, rhs: OSVersion) -> Bool { 63 | return (lhs.major, lhs.minor, lhs.patch) == (rhs.major, rhs.minor, rhs.patch) 64 | } 65 | 66 | public static func < (lhs: OSVersion, rhs: OSVersion) -> Bool { 67 | (lhs.major, lhs.minor, lhs.patch) < (rhs.major, rhs.minor, rhs.patch) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Nudge/Utilities/SoftwareUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SoftwareUpdate.swift 3 | // Nudge 4 | // 5 | // Created by Rory Murdock on 2/10/21. 6 | // 7 | 8 | import Foundation 9 | import os 10 | 11 | class SoftwareUpdate { 12 | func list() -> String { 13 | let (output, error, exitCode) = SubProcessUtilities().runProcess(launchPath: "/usr/sbin/softwareupdate", arguments: ["--list", "--all"]) 14 | 15 | if exitCode != 0 { 16 | LogManager.error("Error listing software updates: \(error)", logger: softwareupdateListLog) 17 | return error 18 | } else { 19 | LogManager.info("\(output)", logger: softwareupdateListLog) 20 | return output 21 | } 22 | } 23 | 24 | func download() { 25 | LogManager.notice("enforceMinorUpdates: \(OptionalFeatureVariables.enforceMinorUpdates)", logger: softwareupdateDownloadLog) 26 | 27 | if DeviceManager().getCPUTypeString() == "Apple Silicon" && !AppStateManager().requireMajorUpgrade() { 28 | LogManager.debug("Apple Silicon devices do not support automated softwareupdate downloads for minor updates. Please use MDM for this functionality.", logger: softwareupdateListLog) 29 | return 30 | } 31 | 32 | if AppStateManager().requireMajorUpgrade() { 33 | guard FeatureVariables.actionButtonPath == nil else { return } 34 | 35 | if OptionalFeatureVariables.attemptToFetchMajorUpgrade, !majorUpgradeAppPathExists, !majorUpgradeBackupAppPathExists { 36 | LogManager.notice("Device requires major upgrade - attempting download", logger: softwareupdateListLog) 37 | let (output, error, exitCode) = SubProcessUtilities().runProcess(launchPath: "/usr/sbin/softwareupdate", arguments: ["--fetch-full-installer", "--full-installer-version", nudgePrimaryState.requiredMinimumOSVersion]) 38 | 39 | if exitCode != 0 { 40 | LogManager.error("Error downloading software update: \(error)", logger: softwareupdateDownloadLog) 41 | } else { 42 | LogManager.info("\(output)", logger: softwareupdateDownloadLog) 43 | GlobalVariables.fetchMajorUpgradeSuccessful = true 44 | // Update the state based on the download result 45 | } 46 | } else if majorUpgradeAppPathExists || majorUpgradeBackupAppPathExists { 47 | LogManager.notice("Found major upgrade application or backup - skipping download", logger: softwareupdateListLog) 48 | } 49 | } else { 50 | if OptionalFeatureVariables.disableSoftwareUpdateWorkflow { 51 | LogManager.notice("Skipping running softwareupdate because it's disabled by a preference.", logger: softwareupdateListLog) 52 | return 53 | } 54 | let softwareupdateList = self.list() 55 | let updateLabel = extractUpdateLabel(from: softwareupdateList) 56 | 57 | if !softwareupdateList.contains(nudgePrimaryState.requiredMinimumOSVersion) || updateLabel.isEmpty { 58 | LogManager.notice("Software update did not find \(nudgePrimaryState.requiredMinimumOSVersion) available for download - skipping download attempt", logger: softwareupdateListLog) 59 | return 60 | } 61 | 62 | LogManager.notice("Software update found \(updateLabel) available for download - attempting download", logger: softwareupdateListLog) 63 | let (output, error, exitCode) = SubProcessUtilities().runProcess(launchPath: "/usr/sbin/softwareupdate", arguments: ["--download", updateLabel]) 64 | 65 | if exitCode != 0 { 66 | LogManager.error("Error downloading software updates: \(error)", logger: softwareupdateDownloadLog) 67 | } else { 68 | LogManager.info("\(output)", logger: softwareupdateDownloadLog) 69 | } 70 | } 71 | } 72 | 73 | private func extractUpdateLabel(from softwareupdateList: String) -> String { 74 | let lines = softwareupdateList.split(separator: "\n") 75 | var updateLabel: String? 76 | 77 | for line in lines { 78 | if line.contains("Label:") { 79 | let labelPart = line.split(separator: ":").map { $0.trimmingCharacters(in: .whitespaces) } 80 | if labelPart.count > 1 && labelPart[1].contains(nudgePrimaryState.requiredMinimumOSVersion) { 81 | updateLabel = labelPart[1] 82 | break 83 | } 84 | } 85 | } 86 | 87 | return updateLabel ?? "" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Nudge/com.github.macadmins.Nudge.SMAppService.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AssociatedBundleIdentifiers 6 | 7 | com.github.macadmins.Nudge 8 | 9 | Label 10 | com.github.macadmins.Nudge.SMAppService 11 | LimitLoadToSessionType 12 | 13 | Aqua 14 | 15 | ProgramArguments 16 | 17 | /Applications/Utilities/Nudge.app/Contents/MacOS/Nudge 18 | 19 | RunAtLoad 20 | 21 | StartCalendarInterval 22 | 23 | 24 | Minute 25 | 0 26 | 27 | 28 | Minute 29 | 30 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /NudgeTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleVersion 20 | 1.0.0 21 | 22 | 23 | -------------------------------------------------------------------------------- /NudgeTests/NudgeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NudgeTests.swift 3 | // NudgeTests 4 | // 5 | // Created by Erik Gomez on 2/2/21. 6 | // 7 | 8 | // TODO: Refactor and rewrite all tests 9 | 10 | import XCTest 11 | @testable import Nudge 12 | 13 | var defaultPreferencesForTests = [:] as [String : Any] 14 | 15 | class NudgeTests: XCTestCase { 16 | func coerceStringToDate(dateString: String) -> Date { 17 | DateManager().dateFormatterISO8601.date(from: dateString) ?? DateManager().getCurrentDate() 18 | } 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | super.tearDown() 28 | } 29 | 30 | func testAllowGracePeriods() { 31 | defaultPreferencesForTests["allowGracePeriods"] = true 32 | PrefsWrapper.prefsOverride = defaultPreferencesForTests 33 | XCTAssertEqual( 34 | true, 35 | PrefsWrapper.allowGracePeriods 36 | ) 37 | } 38 | 39 | func testRequiredMinimumOSVersion() { 40 | defaultPreferencesForTests["requiredMinimumOSVersion"] = "99.99.99" 41 | PrefsWrapper.prefsOverride = defaultPreferencesForTests 42 | XCTAssertEqual( 43 | "99.99.99", 44 | PrefsWrapper.requiredMinimumOSVersion 45 | ) 46 | } 47 | 48 | func testRequiredInstallationDateDemoMode() { 49 | defaultPreferencesForTests["requiredInstallationDate"] = Date(timeIntervalSince1970: 0) 50 | PrefsWrapper.prefsOverride = defaultPreferencesForTests 51 | XCTAssertEqual( 52 | Date(timeIntervalSince1970: 0), 53 | PrefsWrapper.requiredInstallationDate 54 | ) 55 | } 56 | 57 | func testRequiredInstallationDate() { 58 | let testDate = coerceStringToDate(dateString: "2022-02-28T00:00:00Z") 59 | defaultPreferencesForTests["requiredInstallationDate"] = testDate 60 | PrefsWrapper.prefsOverride = defaultPreferencesForTests 61 | XCTAssertEqual( 62 | testDate, 63 | PrefsWrapper.requiredInstallationDate 64 | ) 65 | } 66 | 67 | // Machine is out of date and within requiredInstallationDate 68 | func testGracePeriodInTheMiddleOfNudgeEvent() { 69 | defaultPreferencesForTests["allowGracePeriods"] = true 70 | defaultPreferencesForTests["requiredInstallationDate"] = coerceStringToDate(dateString: "2022-02-01T00:00:00Z") 71 | defaultPreferencesForTests["requiredMinimumOSVersion"] = "99.99.99" 72 | PrefsWrapper.prefsOverride = defaultPreferencesForTests 73 | XCTAssertEqual( 74 | coerceStringToDate(dateString: "2022-02-01T00:00:00Z"), 75 | AppStateManager().gracePeriodLogic( 76 | currentDate: coerceStringToDate(dateString: "2022-01-15T00:00:00Z"), 77 | testFileDate: coerceStringToDate(dateString: "2022-01-01T00:00:00Z") 78 | ) 79 | ) 80 | } 81 | 82 | // Machine is out of date and outside requiredInstallationDate, but just provisioned 83 | func testGracePeriodOutsideNudgeEvent() { 84 | defaultPreferencesForTests["allowGracePeriods"] = true 85 | defaultPreferencesForTests["requiredInstallationDate"] = coerceStringToDate(dateString: "2022-01-01T00:00:00Z") 86 | defaultPreferencesForTests["requiredMinimumOSVersion"] = "99.99.99" 87 | PrefsWrapper.prefsOverride = defaultPreferencesForTests 88 | XCTAssertEqual( 89 | coerceStringToDate(dateString: "2022-01-02T00:30:00Z"), 90 | AppStateManager().gracePeriodLogic( 91 | currentDate: coerceStringToDate(dateString: "2022-01-02T00:30:00Z"), 92 | testFileDate: coerceStringToDate(dateString: "2022-01-02T00:00:00Z") 93 | ) 94 | ) 95 | } 96 | 97 | // Machine was provisioned, but the user ignored the grace period. 98 | // Code reverts back to initial date because UX doesn't matter 99 | func testGracePeriodUserIgnoringGracePeriod() { 100 | defaultPreferencesForTests["allowGracePeriods"] = true 101 | defaultPreferencesForTests["requiredInstallationDate"] = coerceStringToDate(dateString: "2022-01-01T00:00:00Z") 102 | defaultPreferencesForTests["requiredMinimumOSVersion"] = "99.99.99" 103 | PrefsWrapper.prefsOverride = defaultPreferencesForTests 104 | XCTAssertEqual( 105 | coerceStringToDate(dateString: "2022-01-01T00:00:00Z"), 106 | AppStateManager().gracePeriodLogic( 107 | currentDate: coerceStringToDate(dateString: "2022-01-03T:00:00Z"), 108 | testFileDate: coerceStringToDate(dateString: "2022-01-01T00:00:00Z") 109 | ) 110 | ) 111 | } 112 | 113 | // Machine was provisioned, then sat on a shelf for months and now outside requiredInstallationDate 114 | // Your service desk should update the machine before handing it off instead to help the user 115 | func testGracePeriodSittingOnShelfForMonths() { 116 | defaultPreferencesForTests["allowGracePeriods"] = true 117 | defaultPreferencesForTests["requiredInstallationDate"] = coerceStringToDate(dateString: "2022-01-01T00:00:00Z") 118 | defaultPreferencesForTests["requiredMinimumOSVersion"] = "99.99.99" 119 | PrefsWrapper.prefsOverride = defaultPreferencesForTests 120 | XCTAssertEqual( 121 | coerceStringToDate(dateString: "2022-01-01T00:00:00Z"), 122 | AppStateManager().gracePeriodLogic( 123 | currentDate: coerceStringToDate(dateString: "2022-06-01T00:00:00Z"), 124 | testFileDate: coerceStringToDate(dateString: "2021-12-01T00:00:00Z") 125 | ) 126 | ) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /NudgeTests/OSVersionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSVersionTests.swift 3 | // NudgeTests 4 | // 5 | // Created by Victor Vrantchan on 2/5/21. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | @testable import Nudge 11 | 12 | class OSVersionTest: XCTestCase { 13 | func testCompare() { 14 | let a = OSVersion(major: 11, minor: 2, patch: 0) 15 | let b = OSVersion(major: 11, minor: 2, patch: 0) 16 | let c = OSVersion(major: 11, minor: 3, patch: 0) 17 | let d = OSVersion(major: 10, minor: 15, patch: 9999) 18 | 19 | XCTAssertEqual(a,b) 20 | XCTAssertNotEqual(a,c) 21 | XCTAssertGreaterThan(c,b) 22 | XCTAssertGreaterThanOrEqual(c,d) 23 | XCTAssertFalse(a < d, "BigSur is newer than Catalina") 24 | } 25 | 26 | func testParse() { 27 | let expected = OSVersion(major: 11, minor: 5, patch: 0) 28 | guard let actual = try? OSVersion("11.5") else { 29 | XCTFail("expected OSVersion to not fail parsing '11.5'") 30 | return 31 | } 32 | 33 | XCTAssertEqual(expected, actual) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NudgeUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleVersion 20 | 1.0.0 21 | 22 | 23 | -------------------------------------------------------------------------------- /NudgeUITests/NudgeUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NudgeUITests.swift 3 | // NudgeUITests 4 | // 5 | // Created by Erik Gomez on 2/2/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class NudgeUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // 23 | } 24 | 25 | // func testLaunch() throws { 26 | // // UI tests must launch the application that they test. 27 | // let app = XCUIApplication() 28 | // app.launchArguments = ["-demo-mode"] 29 | // app.launch() 30 | // app.terminate() 31 | // // Use recording to get started writing UI tests. 32 | // // Use XCTAssert and related functions to verify your tests produce the correct results. 33 | // } 34 | // 35 | // func testLaunchPerformance() throws { 36 | // if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 37 | // // This measures how long it takes to launch your application. 38 | // measure(metrics: [XCTApplicationLaunchMetric()]) { 39 | // let app = XCUIApplication() 40 | // app.launchArguments = ["-demo-mode"] 41 | // app.launch() 42 | // } 43 | // } 44 | // } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Nudge (macadmin's Slack [#nudge](https://macadmins.slack.com/archives/CDUU7DJQ2)) 4 | Nudge is a multi-linguistic application, offering custom user deferrals, which strongly encourages macOS updates. Written in Swift and SwiftUI. 5 | 6 | Nudge will only work on macOS Big Sur 11 and later and is a replacement for the original Nudge, which was written in Python 2/3. If you need to enforce macOS updates for earlier versions, it is recommended to use [nudge-python](https://github.com/macadmins/nudge-python). 7 | 8 | Nudge 2.0 enables fully automated updates, by default, by querying the [Simple Organized Feed for Apple Software Updates](https://sofa.macadmins.io/) (SOFA) for macOS releases. 9 | 10 | For more information about installation, deployment, and the user experience, please see the [wiki](https://github.com/macadmins/nudge/wiki). 11 | 12 | # Examples of the User Interface 13 | ## simpleMode 14 | ### English 15 | #### Light 16 | 17 | 18 | 19 | #### Dark 20 | 21 | 22 | 23 | ### Localized (Spanish) 24 | #### Light 25 | 26 | 27 | #### Dark 28 | 29 | 30 | ## standardMode 31 | ### English 32 | #### Light 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | #### Dark 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ### Localized (Spanish) 49 | #### Light 50 | 51 | 52 | 53 | #### Dark 54 | 55 | 56 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | While Nudge follows semantic versioning, only the most recent release is supported by the author. 5 | 6 | | Version | Supported | 7 | | ---------- | ------------------ | 8 | | Current | :white_check_mark: | 9 | | < Current | :x: | 10 | 11 | ## Responsible Disclosure 12 | We will honor and expect a reporter to adhere to Google's Disclosure Policy - specifically Google Project Zero. Please see [this link](https://about.google/appsecurity/) for more information on how Google's general process is maintained and [this link](https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html) for information on Google Project Zero. 13 | 14 | ## Guidelines 15 | Under this policy, "research" means activities in which you: 16 | 17 | - Notify us as soon as possible after you discover a real or potential security issue. 18 | - Make every effort to avoid privacy violations, degradation of user experience, disruption to production systems, and destruction or manipulation of data. 19 | - Only use exploits to the extent necessary to confirm a vulnerability's presence. Do not use an exploit to compromise or exfiltrate data, establish persistent command line access, or use the exploit to pivot to other systems. 20 | - **Provide us a reasonable amount of time to resolve the issue before you disclose it publicly.** 21 | - Do not submit a high volume of low-quality reports. 22 | 23 | ## Reporting a Vulnerability 24 | 25 | Please report all vulnerabilities to **security@macadmins.io**. Reports may be submitted anonymously. If you share contact information, we will acknowledge receipt of your report within 3 business days. We do not support PGP-encrypted emails at this time. 26 | 27 | ### What we would like to see from you 28 | In order to help us triage and prioritize submissions, we recommend that your reports: 29 | 30 | - Describe the location the vulnerability was discovered and the potential impact of exploitation. 31 | - Offer a detailed description of the steps needed to reproduce the vulnerability (proof of concept scripts or screenshots are helpful). 32 | - Be in English, if possible. 33 | 34 | ### What you can expect from us 35 | When you choose to share your contact information with us, we commit to coordinating with you as openly and as quickly as possible. 36 | 37 | - Within 3 business days, we will acknowledge that your report has been received. 38 | - To the best of our ability, we will confirm the existence of the vulnerability to you and be as transparent as possible about what steps we are taking during the remediation process, including on issues or challenges that may delay resolution. 39 | - We will maintain an open dialogue to discuss issues. 40 | 41 | Nudge is maintained almost exclusively by a single author, Erik Gomez. Upon reporting the vulnerability, please expect at least an additional 2-3 business days to assess the report. This time is loose and may be shorter or longer depending on availability of the author. 42 | 43 | Upon review, the vulnerability will be assigned the following four levels: Low, Medium, High and Critical. 44 | 45 | SLAs will be currently defined per the following guidelines 46 | 47 | | Criticality | Time to remediation | 48 | | ----------- | ------------------- | 49 | | Critical | 7 Business Days | 50 | | High | 14 Business Days | 51 | | Medium | 30 Business Days | 52 | | Low | 90 Business Days | 53 | 54 | ## Payment 55 | At this time, we are not able to compensate for any reports submitted. 56 | 57 | ## Version History 58 | | Version | Date | Description | 59 | | ------- | ---------- | ------------------- | 60 | | 1.0 | 03/01/2024 | First issuance | 61 | -------------------------------------------------------------------------------- /assets/NudgeCustomLogoGuide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/NudgeCustomLogoGuide.png -------------------------------------------------------------------------------- /assets/NudgeCustomLogoGuide.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/NudgeCustomLogoGuide.pxm -------------------------------------------------------------------------------- /assets/NudgeIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/NudgeIcon.png -------------------------------------------------------------------------------- /assets/NudgeIcon.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/NudgeIcon.pxm -------------------------------------------------------------------------------- /assets/NudgeIconInverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/NudgeIconInverted.png -------------------------------------------------------------------------------- /assets/simple_mode/demo_simple_dark_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/simple_mode/demo_simple_dark_1.png -------------------------------------------------------------------------------- /assets/simple_mode/demo_simple_dark_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/simple_mode/demo_simple_dark_2.png -------------------------------------------------------------------------------- /assets/simple_mode/demo_simple_dark_localized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/simple_mode/demo_simple_dark_localized.png -------------------------------------------------------------------------------- /assets/simple_mode/demo_simple_light_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/simple_mode/demo_simple_light_1.png -------------------------------------------------------------------------------- /assets/simple_mode/demo_simple_light_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/simple_mode/demo_simple_light_2.png -------------------------------------------------------------------------------- /assets/simple_mode/demo_simple_light_localized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/simple_mode/demo_simple_light_localized.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_dark_1_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_dark_1_icon.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_dark_1_no_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_dark_1_no_icon.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_dark_2_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_dark_2_icon.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_dark_2_icon_localized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_dark_2_icon_localized.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_dark_2_no_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_dark_2_no_icon.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_dark_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_dark_3.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_dark_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_dark_4.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_dark_4_localized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_dark_4_localized.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_light_1_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_light_1_icon.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_light_1_no_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_light_1_no_icon.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_light_2_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_light_2_icon.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_light_2_localized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_light_2_localized.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_light_2_no_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_light_2_no_icon.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_light_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_light_3.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_light_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_light_4.png -------------------------------------------------------------------------------- /assets/standard_mode/demo_light_4_icon_localized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/nudge/533186bc3a3f28c7a017e957f86b8787346bce15/assets/standard_mode/demo_light_4_icon_localized.png -------------------------------------------------------------------------------- /build_assets/com.github.macadmins.Nudge.logger.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.github.macadmins.Nudge.Logger 7 | ProgramArguments 8 | 9 | /usr/bin/log 10 | stream 11 | --predicate 12 | subsystem == 'com.github.macadmins.Nudge' 13 | --style 14 | syslog 15 | --color 16 | none 17 | 18 | RunAtLoad 19 | 20 | StandardOutPath 21 | /var/log/Nudge.log 22 | 23 | 24 | -------------------------------------------------------------------------------- /build_assets/com.github.macadmins.Nudge.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AssociatedBundleIdentifiers 6 | 7 | com.github.macadmins.Nudge 8 | 9 | Label 10 | com.github.macadmins.Nudge 11 | LimitLoadToSessionType 12 | 13 | Aqua 14 | 15 | ProgramArguments 16 | 17 | /Applications/Utilities/Nudge.app/Contents/MacOS/Nudge 18 | 19 | 20 | 21 | 22 | RunAtLoad 23 | 24 | StartCalendarInterval 25 | 26 | 27 | Minute 28 | 0 29 | 30 | 31 | Minute 32 | 30 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /build_assets/postinstall-essentials: -------------------------------------------------------------------------------- 1 | #!/bin/zsh --no-rcs 2 | # 3 | # Copyright 2021-Present Erik Gomez. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the 'License'); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an 'AS IS' BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Only run if on a running system 18 | if [[ $3 == "/" ]] ; then 19 | /bin/zsh --no-rcs -c '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-nudge' 20 | /bin/zsh --no-rcs -c '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-launchagent' 21 | fi 22 | -------------------------------------------------------------------------------- /build_assets/postinstall-launchagent: -------------------------------------------------------------------------------- 1 | #!/bin/zsh --no-rcs 2 | # 3 | # Copyright 2021-Present Erik Gomez. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the 'License'); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an 'AS IS' BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Only run if on a running system 18 | if [[ $3 == "/" ]] ; then 19 | if [ -f '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-launchagent' ]; then 20 | /bin/zsh --no-rcs -c '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-launchagent' 21 | else 22 | echo "File does not exist: Please ensure Nudge is installed prior to installation of these packages" 23 | fi 24 | fi 25 | -------------------------------------------------------------------------------- /build_assets/postinstall-logger: -------------------------------------------------------------------------------- 1 | #!/bin/zsh --no-rcs 2 | # 3 | # Copyright 2021-Present Erik Gomez. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the 'License'); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an 'AS IS' BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Only run if on a running system 18 | if [[ $3 == "/" ]] ; then 19 | if [ -f '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-logger' ]; then 20 | /bin/zsh --no-rcs -c '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-logger' 21 | else 22 | echo "File does not exist: Please ensure Nudge is installed prior to installation of these packages" 23 | fi 24 | fi 25 | -------------------------------------------------------------------------------- /build_assets/postinstall-nudge: -------------------------------------------------------------------------------- 1 | #!/bin/zsh --no-rcs 2 | # 3 | # Copyright 2021-Present Erik Gomez. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the 'License'); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an 'AS IS' BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Only run if on a running system 18 | if [[ $3 == "/" ]] ; then 19 | /bin/zsh --no-rcs -c '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-nudge' 20 | fi 21 | -------------------------------------------------------------------------------- /build_assets/postinstall-suite: -------------------------------------------------------------------------------- 1 | #!/bin/zsh --no-rcs 2 | # 3 | # Copyright 2021-Present Erik Gomez. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the 'License'); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an 'AS IS' BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Only run if on a running system 18 | if [[ $3 == "/" ]] ; then 19 | /bin/zsh --no-rcs -c '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-nudge' 20 | /bin/zsh --no-rcs -c '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-logger' 21 | /bin/zsh --no-rcs -c '/Applications/Utilities/Nudge.app/Contents/Resources/postinstall-launchagent' 22 | fi 23 | --------------------------------------------------------------------------------