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