├── .gitignore ├── .swift-version ├── BuildTools ├── Package.resolved └── Package.swift ├── CHANGELOG ├── Gemfile ├── Gemfile.lock ├── JWT ├── .gitignore ├── Package.swift ├── README.md ├── Sources │ └── JWT │ │ └── JWT.swift └── Tests │ └── JWTTests │ └── JWTTests.swift ├── LICENSE ├── README.md ├── Swush.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── Swush - Debug.xcscheme │ └── Swush - Release.xcscheme ├── Swush ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-1024.png │ │ ├── Icon-128.png │ │ ├── Icon-16.png │ │ ├── Icon-256.png │ │ ├── Icon-257.png │ │ ├── Icon-32.png │ │ ├── Icon-33.png │ │ ├── Icon-512.png │ │ ├── Icon-513.png │ │ └── Icon-64.png │ └── Contents.json ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Sources │ ├── App │ │ ├── AppState │ │ │ ├── AppState+Creation.swift │ │ │ ├── AppState+Deletion.swift │ │ │ ├── AppState+Renaming.swift │ │ │ ├── AppState+SendPush.swift │ │ │ └── AppState.swift │ │ └── SwushApp.swift │ ├── Components │ │ └── Input.swift │ ├── Database │ │ ├── AppDatabase.swift │ │ └── Persistence.swift │ ├── Extensions │ │ ├── NSTextView+Extensions.swift │ │ ├── Published+Extensions.swift │ │ ├── SecIdentity+Extensions.swift │ │ ├── String+Extensions.swift │ │ ├── URLResponse+Extensions.swift │ │ ├── View+ConditionalModifier.swift │ │ └── View+NSCursor.swift │ ├── Models │ │ ├── APNS+CertificateType.swift │ │ ├── APNS+IdentityType.swift │ │ ├── APNS+PayloadType.swift │ │ ├── APNS+Priority.swift │ │ ├── APNS.swift │ │ └── SecIdentityType.swift │ ├── Modules │ │ ├── ApnsList │ │ │ └── ApnsListView.swift │ │ ├── Commands │ │ │ ├── CreateApnsView.swift │ │ │ ├── DeleteApnsView.swift │ │ │ ├── SaveApnsView.swift │ │ │ └── Updater │ │ │ │ ├── CheckForUpdatesView.swift │ │ │ │ └── UpdaterViewModel.swift │ │ ├── ContentView.swift │ │ ├── Sender │ │ │ └── SenderView.swift │ │ └── Settings │ │ │ ├── GeneralSettingsView.swift │ │ │ └── SettingsView.swift │ └── Services │ │ ├── APNSService+Error.swift │ │ ├── APNSService.swift │ │ ├── DependencyProvider.swift │ │ └── SecIdentityService.swift └── Swush.entitlements ├── fastlane ├── Appfile ├── Fastfile ├── README.md └── generate_appcast ├── icon.png └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two \r 8 | Icon 9 | 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | ### Swift ### 31 | # Xcode 32 | # 33 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 34 | 35 | ## User settings 36 | xcuserdata/ 37 | 38 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 39 | *.xcscmblueprint 40 | *.xccheckout 41 | 42 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 43 | build/ 44 | DerivedData/ 45 | *.moved-aside 46 | *.pbxuser 47 | !default.pbxuser 48 | *.mode1v3 49 | !default.mode1v3 50 | *.mode2v3 51 | !default.mode2v3 52 | *.perspectivev3 53 | !default.perspectivev3 54 | 55 | ## Obj-C/Swift specific 56 | *.hmap 57 | 58 | ## App packaging 59 | *.ipa 60 | *.dSYM.zip 61 | *.dSYM 62 | 63 | ## Playgrounds 64 | timeline.xctimeline 65 | playground.xcworkspace 66 | 67 | # Swift Package Manager 68 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 69 | # Packages/ 70 | # Package.pins 71 | # Package.resolved 72 | # *.xcodeproj 73 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 74 | # hence it is not needed unless you have added a package configuration file to your project 75 | # .swiftpm 76 | 77 | .build/ 78 | 79 | # CocoaPods 80 | # We recommend against adding the Pods directory to your .gitignore. However 81 | # you should judge for yourself, the pros and cons are mentioned at: 82 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 83 | # Pods/ 84 | # Add this line if you want to avoid checking in source code from the Xcode workspace 85 | # *.xcworkspace 86 | 87 | # Carthage 88 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 89 | # Carthage/Checkouts 90 | 91 | Carthage/Build/ 92 | 93 | # Accio dependency management 94 | Dependencies/ 95 | .accio/ 96 | 97 | # fastlane 98 | # It is recommended to not store the screenshots in the git repo. 99 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 100 | # For more information about the recommended setup visit: 101 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 102 | 103 | fastlane/report.xml 104 | fastlane/Preview.html 105 | fastlane/screenshots/**/*.png 106 | fastlane/test_output 107 | 108 | # Code Injection 109 | # After new code Injection tools there's a generated folder /iOSInjectionProject 110 | # https://github.com/johnno1962/injectionforxcode 111 | 112 | iOSInjectionProject/ 113 | 114 | ### Xcode ### 115 | 116 | ## Xcode 8 and earlier 117 | 118 | ### Xcode Patch ### 119 | *.xcodeproj/* 120 | !*.xcodeproj/project.pbxproj 121 | !*.xcodeproj/xcshareddata/ 122 | !*.xcworkspace/contents.xcworkspacedata 123 | /*.gcno 124 | **/xcshareddata/WorkspaceSettings.xcsettings 125 | Build/ 126 | Releases/ 127 | .env 128 | BuildTools/.build 129 | BuildTools/.swiftpm -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.5 -------------------------------------------------------------------------------- /BuildTools/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SwiftFormat", 6 | "repositoryURL": "https://github.com/nicklockwood/SwiftFormat", 7 | "state": { 8 | "branch": null, 9 | "revision": "637394feb9470cac534f3669c7c73745c6fdfaf0", 10 | "version": "0.49.3" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /BuildTools/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "BuildTools", 6 | platforms: [.macOS(.v10_11)], 7 | dependencies: [ 8 | .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.49.0"), 9 | ], 10 | targets: [.target(name: "BuildTools", path: "")] 11 | ) 12 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # 🐛 Bug fixes 2 | 3 | - Fixed bug on renaming 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | addressable (2.8.0) 7 | public_suffix (>= 2.0.2, < 5.0) 8 | artifactory (3.0.15) 9 | atomos (0.1.3) 10 | aws-eventstream (1.2.0) 11 | aws-partitions (1.551.0) 12 | aws-sdk-core (3.125.5) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.525.0) 15 | aws-sigv4 (~> 1.1) 16 | jmespath (~> 1.0) 17 | aws-sdk-kms (1.53.0) 18 | aws-sdk-core (~> 3, >= 3.125.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.111.3) 21 | aws-sdk-core (~> 3, >= 3.125.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.4) 24 | aws-sigv4 (1.4.0) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.1.0) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | digest-crc (0.6.4) 34 | rake (>= 12.0.0, < 14.0.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.7.6) 38 | emoji_regex (3.2.3) 39 | excon (0.90.0) 40 | faraday (1.9.3) 41 | faraday-em_http (~> 1.0) 42 | faraday-em_synchrony (~> 1.0) 43 | faraday-excon (~> 1.1) 44 | faraday-httpclient (~> 1.0) 45 | faraday-multipart (~> 1.0) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.0) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | faraday-retry (~> 1.0) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-multipart (1.0.3) 60 | multipart-post (>= 1.2, < 3) 61 | faraday-net_http (1.0.1) 62 | faraday-net_http_persistent (1.2.0) 63 | faraday-patron (1.0.0) 64 | faraday-rack (1.0.0) 65 | faraday-retry (1.0.3) 66 | faraday_middleware (1.2.0) 67 | faraday (~> 1.0) 68 | fastimage (2.2.6) 69 | fastlane (2.203.0) 70 | CFPropertyList (>= 2.3, < 4.0.0) 71 | addressable (>= 2.8, < 3.0.0) 72 | artifactory (~> 3.0) 73 | aws-sdk-s3 (~> 1.0) 74 | babosa (>= 1.0.3, < 2.0.0) 75 | bundler (>= 1.12.0, < 3.0.0) 76 | colored 77 | commander (~> 4.6) 78 | dotenv (>= 2.1.1, < 3.0.0) 79 | emoji_regex (>= 0.1, < 4.0) 80 | excon (>= 0.71.0, < 1.0.0) 81 | faraday (~> 1.0) 82 | faraday-cookie_jar (~> 0.0.6) 83 | faraday_middleware (~> 1.0) 84 | fastimage (>= 2.1.0, < 3.0.0) 85 | gh_inspector (>= 1.1.2, < 2.0.0) 86 | google-apis-androidpublisher_v3 (~> 0.3) 87 | google-apis-playcustomapp_v1 (~> 0.1) 88 | google-cloud-storage (~> 1.31) 89 | highline (~> 2.0) 90 | json (< 3.0.0) 91 | jwt (>= 2.1.0, < 3) 92 | mini_magick (>= 4.9.4, < 5.0.0) 93 | multipart-post (~> 2.0.0) 94 | naturally (~> 2.2) 95 | optparse (~> 0.1.1) 96 | plist (>= 3.1.0, < 4.0.0) 97 | rubyzip (>= 2.0.0, < 3.0.0) 98 | security (= 0.1.3) 99 | simctl (~> 1.6.3) 100 | terminal-notifier (>= 2.0.0, < 3.0.0) 101 | terminal-table (>= 1.4.5, < 2.0.0) 102 | tty-screen (>= 0.6.3, < 1.0.0) 103 | tty-spinner (>= 0.8.0, < 1.0.0) 104 | word_wrap (~> 1.0.0) 105 | xcodeproj (>= 1.13.0, < 2.0.0) 106 | xcpretty (~> 0.3.0) 107 | xcpretty-travis-formatter (>= 0.0.3) 108 | gh_inspector (1.1.3) 109 | google-apis-androidpublisher_v3 (0.15.0) 110 | google-apis-core (>= 0.4, < 2.a) 111 | google-apis-core (0.4.2) 112 | addressable (~> 2.5, >= 2.5.1) 113 | googleauth (>= 0.16.2, < 2.a) 114 | httpclient (>= 2.8.1, < 3.a) 115 | mini_mime (~> 1.0) 116 | representable (~> 3.0) 117 | retriable (>= 2.0, < 4.a) 118 | rexml 119 | webrick 120 | google-apis-iamcredentials_v1 (0.10.0) 121 | google-apis-core (>= 0.4, < 2.a) 122 | google-apis-playcustomapp_v1 (0.7.0) 123 | google-apis-core (>= 0.4, < 2.a) 124 | google-apis-storage_v1 (0.11.0) 125 | google-apis-core (>= 0.4, < 2.a) 126 | google-cloud-core (1.6.0) 127 | google-cloud-env (~> 1.0) 128 | google-cloud-errors (~> 1.0) 129 | google-cloud-env (1.5.0) 130 | faraday (>= 0.17.3, < 2.0) 131 | google-cloud-errors (1.2.0) 132 | google-cloud-storage (1.36.0) 133 | addressable (~> 2.8) 134 | digest-crc (~> 0.4) 135 | google-apis-iamcredentials_v1 (~> 0.1) 136 | google-apis-storage_v1 (~> 0.1) 137 | google-cloud-core (~> 1.6) 138 | googleauth (>= 0.16.2, < 2.a) 139 | mini_mime (~> 1.0) 140 | googleauth (1.1.0) 141 | faraday (>= 0.17.3, < 2.0) 142 | jwt (>= 1.4, < 3.0) 143 | memoist (~> 0.16) 144 | multi_json (~> 1.11) 145 | os (>= 0.9, < 2.0) 146 | signet (>= 0.16, < 2.a) 147 | highline (2.0.3) 148 | http-cookie (1.0.4) 149 | domain_name (~> 0.5) 150 | httpclient (2.8.3) 151 | jmespath (1.5.0) 152 | json (2.6.1) 153 | jwt (2.3.0) 154 | memoist (0.16.2) 155 | mini_magick (4.11.0) 156 | mini_mime (1.1.2) 157 | multi_json (1.15.0) 158 | multipart-post (2.0.0) 159 | nanaimo (0.3.0) 160 | naturally (2.2.1) 161 | optparse (0.1.1) 162 | os (1.1.4) 163 | plist (3.6.0) 164 | public_suffix (4.0.6) 165 | rake (13.0.6) 166 | representable (3.1.1) 167 | declarative (< 0.1.0) 168 | trailblazer-option (>= 0.1.1, < 0.2.0) 169 | uber (< 0.2.0) 170 | retriable (3.1.2) 171 | rexml (3.2.5) 172 | rouge (2.0.7) 173 | ruby2_keywords (0.0.5) 174 | rubyzip (2.3.2) 175 | security (0.1.3) 176 | signet (0.16.0) 177 | addressable (~> 2.8) 178 | faraday (>= 0.17.3, < 2.0) 179 | jwt (>= 1.5, < 3.0) 180 | multi_json (~> 1.10) 181 | simctl (1.6.8) 182 | CFPropertyList 183 | naturally 184 | terminal-notifier (2.0.0) 185 | terminal-table (1.8.0) 186 | unicode-display_width (~> 1.1, >= 1.1.1) 187 | trailblazer-option (0.1.2) 188 | tty-cursor (0.7.1) 189 | tty-screen (0.8.1) 190 | tty-spinner (0.9.3) 191 | tty-cursor (~> 0.7) 192 | uber (0.1.0) 193 | unf (0.1.4) 194 | unf_ext 195 | unf_ext (0.0.8) 196 | unicode-display_width (1.8.0) 197 | webrick (1.7.0) 198 | word_wrap (1.0.0) 199 | xcodeproj (1.21.0) 200 | CFPropertyList (>= 2.3.3, < 4.0) 201 | atomos (~> 0.1.3) 202 | claide (>= 1.0.2, < 2.0) 203 | colored2 (~> 3.1) 204 | nanaimo (~> 0.3.0) 205 | rexml (~> 3.2.4) 206 | xcpretty (0.3.0) 207 | rouge (~> 2.0.7) 208 | xcpretty-travis-formatter (1.0.1) 209 | xcpretty (~> 0.2, >= 0.0.7) 210 | 211 | PLATFORMS 212 | arm64-darwin-21 213 | 214 | DEPENDENCIES 215 | fastlane 216 | 217 | BUNDLED WITH 218 | 2.3.4 219 | -------------------------------------------------------------------------------- /JWT/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /JWT/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "JWT", 8 | platforms: [ 9 | .macOS(.v11) 10 | ], 11 | products: [ 12 | .library( 13 | name: "JWT", 14 | targets: ["JWT"]), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "JWT", 19 | dependencies: []), 20 | .testTarget( 21 | name: "JWTTests", 22 | dependencies: ["JWT"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /JWT/README.md: -------------------------------------------------------------------------------- 1 | # JWT 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /JWT/Sources/JWT/JWT.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func safeShell(_ command: String) throws -> String { 4 | let task = Process() 5 | let pipe = Pipe() 6 | 7 | task.standardOutput = pipe 8 | task.standardError = pipe 9 | task.arguments = ["-c", command] 10 | task.executableURL = URL(fileURLWithPath: "/bin/zsh") //<--updated 11 | 12 | do { 13 | try task.run() //<--updated 14 | } 15 | catch{ throw error } 16 | 17 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 18 | let output = String(data: data, encoding: .utf8)! 19 | 20 | return output 21 | } 22 | 23 | 24 | 25 | public struct JWT { 26 | private let teamId: String 27 | private let topic: String 28 | private let keyId: String 29 | private let tokenFilename: String 30 | 31 | public init(teamId: String, topic: String, keyId: String, tokenFilename: String) { 32 | self.teamId = teamId 33 | self.topic = topic 34 | self.keyId = keyId 35 | self.tokenFilename = tokenFilename 36 | } 37 | 38 | public var token: String { 39 | let header = try! safeShell("printf '{ \"alg\": \"ES256\", \"kid\": \"%s\" }' \"\(keyId)\" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =") 40 | let claims = try! safeShell("printf '{ \"iss\": \"%s\", \"iat\": %d }' \"\(teamId)\" \"\(Date().timeIntervalSince1970)\" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =") 41 | let headerClaims = "\(header).\(claims)" 42 | let signedHeaderClaims = try! safeShell("printf \"\(headerClaims)\" | openssl dgst -binary -sha256 -sign \"\(tokenFilename)\" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =") 43 | return "\(header).\(claims).\(signedHeaderClaims)" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /JWT/Tests/JWTTests/JWTTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import JWT 3 | 4 | final class JWTTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(JWT().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Quentin Eude 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **THIS REPOSITORY IS ARCHIVED SINCE THERE IS [AN OFFICIAL PUSH CONSOLE](https://developer.apple.com/documentation/usernotifications/testing_notifications_using_the_push_notification_console) THAT EXISTS NOW.** 2 | 3 |
4 |

Swush

5 |
6 | 7 | ## ✨ Description 8 | 9 | A macOS app to push notifications to APNS with ease. ⚡ 10 | 11 | - 💾 Persisted push for easy replay. 12 | - 🔑 Handle both `.p12` certificates and `.p8` tokens. 13 | - 🎟️ Automatically retrieve APNS certificates from your keychain. 14 | - ❤️ Made using SwiftUI. 15 | 16 | ## 🚀 Installation 17 | 18 | Download the latest release [here](https://github.com/qeude/Swush/releases) and all done 🙌.. 19 | 20 | ## 🧑‍⚖️ License 21 | 22 | Swush is licensed under [The MIT Licence (MIT)](LICENSE). 23 | 24 | ## 👨🏻‍💻 Developer 25 | 26 | - Quentin Eude 27 | - [Github](https://github.com/qeude) 28 | - [Twitter](https://twitter.com/q_eude) 29 | - [LinkedIn](https://www.linkedin.com/in/quentineude/) 30 | -------------------------------------------------------------------------------- /Swush.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0101ACF927B036A50010E212 /* SaveApnsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0101ACF827B036A50010E212 /* SaveApnsView.swift */; }; 11 | 0101ACFC27B03EE50010E212 /* AppState+Renaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0101ACFB27B03EE50010E212 /* AppState+Renaming.swift */; }; 12 | 0101ACFE27B03F850010E212 /* AppState+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0101ACFD27B03F850010E212 /* AppState+Creation.swift */; }; 13 | 0101AD0027B03FBB0010E212 /* AppState+Deletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0101ACFF27B03FBB0010E212 /* AppState+Deletion.swift */; }; 14 | 0101AD0227B03FFC0010E212 /* AppState+SendPush.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0101AD0127B03FFC0010E212 /* AppState+SendPush.swift */; }; 15 | 010E486927A5BA8500D950EF /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010E486827A5BA8500D950EF /* Persistence.swift */; }; 16 | 010E486C27A5BC7500D950EF /* GRDBQuery in Frameworks */ = {isa = PBXBuildFile; productRef = 010E486B27A5BC7500D950EF /* GRDBQuery */; }; 17 | 010E486F27A5BF7C00D950EF /* ApnsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010E486E27A5BF7C00D950EF /* ApnsListView.swift */; }; 18 | 010E487427A69EC400D950EF /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 010E487327A69EC400D950EF /* Sparkle */; }; 19 | 010E487727A69F1A00D950EF /* UpdaterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010E487627A69F1A00D950EF /* UpdaterViewModel.swift */; }; 20 | 010E487927A69F4900D950EF /* CheckForUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010E487827A69F4900D950EF /* CheckForUpdatesView.swift */; }; 21 | 012CEBFC27A7204400D76A5A /* Published+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012CEBFB27A7204400D76A5A /* Published+Extensions.swift */; }; 22 | 012CEBFF27A7221900D76A5A /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012CEBFE27A7221900D76A5A /* SettingsView.swift */; }; 23 | 012CEC0127A7225000D76A5A /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012CEC0027A7225000D76A5A /* GeneralSettingsView.swift */; }; 24 | 012D75CD27A984CC00A55457 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012D75CC27A984CC00A55457 /* AppState.swift */; }; 25 | 013F50A627B2F9BC002B7970 /* APNS+CertificateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013F50A527B2F9BC002B7970 /* APNS+CertificateType.swift */; }; 26 | 0145B11B27A592B800E0EC43 /* APNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B11A27A592B800E0EC43 /* APNS.swift */; }; 27 | 0145B11E27A592E700E0EC43 /* URLResponse+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B11D27A592E700E0EC43 /* URLResponse+Extensions.swift */; }; 28 | 0145B12027A5930800E0EC43 /* APNS+PayloadType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B11F27A5930800E0EC43 /* APNS+PayloadType.swift */; }; 29 | 0145B12327A593A000E0EC43 /* DependencyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B12227A593A000E0EC43 /* DependencyProvider.swift */; }; 30 | 0145B12527A5950400E0EC43 /* SecIdentityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B12427A5950400E0EC43 /* SecIdentityType.swift */; }; 31 | 0145B12727A5973000E0EC43 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B12627A5973000E0EC43 /* String+Extensions.swift */; }; 32 | 0145B12927A5975200E0EC43 /* SenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B12827A5975200E0EC43 /* SenderView.swift */; }; 33 | 0145B12C27A5984D00E0EC43 /* APNS+IdentityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B12B27A5984D00E0EC43 /* APNS+IdentityType.swift */; }; 34 | 0145B12E27A59A3D00E0EC43 /* APNS+Priority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B12D27A59A3D00E0EC43 /* APNS+Priority.swift */; }; 35 | 0145B13027A5A95200E0EC43 /* SecIdentity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0145B12F27A5A95200E0EC43 /* SecIdentity+Extensions.swift */; }; 36 | 0145B13327A5B12300E0EC43 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 0145B13227A5B12300E0EC43 /* GRDB */; }; 37 | 014D0C7327A1AECC00C43A74 /* SwushApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014D0C7227A1AECC00C43A74 /* SwushApp.swift */; }; 38 | 014D0C7527A1AECC00C43A74 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014D0C7427A1AECC00C43A74 /* ContentView.swift */; }; 39 | 014D0C7727A1AECD00C43A74 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 014D0C7627A1AECD00C43A74 /* Assets.xcassets */; }; 40 | 014D0C7A27A1AECD00C43A74 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 014D0C7927A1AECD00C43A74 /* Preview Assets.xcassets */; }; 41 | 014D0C8427A1BCEC00C43A74 /* SecIdentityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014D0C8327A1BCEC00C43A74 /* SecIdentityService.swift */; }; 42 | 014D0C9027A1E61600C43A74 /* APNSService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014D0C8F27A1E61600C43A74 /* APNSService.swift */; }; 43 | 0188035627B949D900823EE6 /* JWT in Frameworks */ = {isa = PBXBuildFile; productRef = 0188035527B949D900823EE6 /* JWT */; }; 44 | 0188035927B953C900823EE6 /* Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188035827B953C900823EE6 /* Input.swift */; }; 45 | 0188035D27B9A5CD00823EE6 /* APNSService+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188035C27B9A5CD00823EE6 /* APNSService+Error.swift */; }; 46 | 0194C7D827A5B55300FB943F /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0194C7D727A5B55300FB943F /* AppDatabase.swift */; }; 47 | 01C1BCC627BC2F9F0080088A /* View+NSCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C1BCC527BC2F9F0080088A /* View+NSCursor.swift */; }; 48 | 01C8269B27A87C450043E890 /* NSTextView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C8269A27A87C450043E890 /* NSTextView+Extensions.swift */; }; 49 | 01C8269E27A87FD70043E890 /* CreateApnsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C8269D27A87FD70043E890 /* CreateApnsView.swift */; }; 50 | 01F8C2E627B04B8300B37588 /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F8C2E527B04B8300B37588 /* View+ConditionalModifier.swift */; }; 51 | 01F8C2E827B04C4E00B37588 /* DeleteApnsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F8C2E727B04C4E00B37588 /* DeleteApnsView.swift */; }; 52 | /* End PBXBuildFile section */ 53 | 54 | /* Begin PBXFileReference section */ 55 | 0101ACF827B036A50010E212 /* SaveApnsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveApnsView.swift; sourceTree = ""; }; 56 | 0101ACFB27B03EE50010E212 /* AppState+Renaming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+Renaming.swift"; sourceTree = ""; }; 57 | 0101ACFD27B03F850010E212 /* AppState+Creation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+Creation.swift"; sourceTree = ""; }; 58 | 0101ACFF27B03FBB0010E212 /* AppState+Deletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+Deletion.swift"; sourceTree = ""; }; 59 | 0101AD0127B03FFC0010E212 /* AppState+SendPush.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+SendPush.swift"; sourceTree = ""; }; 60 | 010E486827A5BA8500D950EF /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; 61 | 010E486E27A5BF7C00D950EF /* ApnsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApnsListView.swift; sourceTree = ""; }; 62 | 010E487627A69F1A00D950EF /* UpdaterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdaterViewModel.swift; sourceTree = ""; }; 63 | 010E487827A69F4900D950EF /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = ""; }; 64 | 012CEBFB27A7204400D76A5A /* Published+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Published+Extensions.swift"; sourceTree = ""; }; 65 | 012CEBFE27A7221900D76A5A /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 66 | 012CEC0027A7225000D76A5A /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; 67 | 012D75CC27A984CC00A55457 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; 68 | 013F50A527B2F9BC002B7970 /* APNS+CertificateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APNS+CertificateType.swift"; sourceTree = ""; }; 69 | 0145B11A27A592B800E0EC43 /* APNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNS.swift; sourceTree = ""; }; 70 | 0145B11D27A592E700E0EC43 /* URLResponse+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLResponse+Extensions.swift"; sourceTree = ""; }; 71 | 0145B11F27A5930800E0EC43 /* APNS+PayloadType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APNS+PayloadType.swift"; sourceTree = ""; }; 72 | 0145B12227A593A000E0EC43 /* DependencyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyProvider.swift; sourceTree = ""; }; 73 | 0145B12427A5950400E0EC43 /* SecIdentityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecIdentityType.swift; sourceTree = ""; }; 74 | 0145B12627A5973000E0EC43 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 75 | 0145B12827A5975200E0EC43 /* SenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderView.swift; sourceTree = ""; }; 76 | 0145B12B27A5984D00E0EC43 /* APNS+IdentityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APNS+IdentityType.swift"; sourceTree = ""; }; 77 | 0145B12D27A59A3D00E0EC43 /* APNS+Priority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APNS+Priority.swift"; sourceTree = ""; }; 78 | 0145B12F27A5A95200E0EC43 /* SecIdentity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecIdentity+Extensions.swift"; sourceTree = ""; }; 79 | 014D0C6F27A1AECC00C43A74 /* Swush.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Swush.app; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | 014D0C7227A1AECC00C43A74 /* SwushApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwushApp.swift; sourceTree = ""; }; 81 | 014D0C7427A1AECC00C43A74 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 82 | 014D0C7627A1AECD00C43A74 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 83 | 014D0C7927A1AECD00C43A74 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 84 | 014D0C7B27A1AECD00C43A74 /* Swush.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swush.entitlements; sourceTree = ""; }; 85 | 014D0C8327A1BCEC00C43A74 /* SecIdentityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecIdentityService.swift; sourceTree = ""; }; 86 | 014D0C8F27A1E61600C43A74 /* APNSService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSService.swift; sourceTree = ""; }; 87 | 015E27AE27B02F80002D50B4 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 88 | 0188035327B9422E00823EE6 /* JWT */ = {isa = PBXFileReference; lastKnownFileType = text; path = JWT; sourceTree = SOURCE_ROOT; }; 89 | 0188035827B953C900823EE6 /* Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Input.swift; sourceTree = ""; }; 90 | 0188035C27B9A5CD00823EE6 /* APNSService+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APNSService+Error.swift"; sourceTree = ""; }; 91 | 0194C7D727A5B55300FB943F /* AppDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = ""; }; 92 | 01C1BCC527BC2F9F0080088A /* View+NSCursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+NSCursor.swift"; sourceTree = ""; }; 93 | 01C8269727A8798D0043E890 /* NSTextView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextView+Extensions.swift"; sourceTree = ""; }; 94 | 01C8269A27A87C450043E890 /* NSTextView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextView+Extensions.swift"; sourceTree = ""; }; 95 | 01C8269D27A87FD70043E890 /* CreateApnsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateApnsView.swift; sourceTree = ""; }; 96 | 01F8C2E527B04B8300B37588 /* View+ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ConditionalModifier.swift"; sourceTree = ""; }; 97 | 01F8C2E727B04C4E00B37588 /* DeleteApnsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteApnsView.swift; sourceTree = ""; }; 98 | 01FFBC7E27A6A6C400D8780C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 99 | /* End PBXFileReference section */ 100 | 101 | /* Begin PBXFrameworksBuildPhase section */ 102 | 014D0C6C27A1AECC00C43A74 /* Frameworks */ = { 103 | isa = PBXFrameworksBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | 0188035627B949D900823EE6 /* JWT in Frameworks */, 107 | 010E487427A69EC400D950EF /* Sparkle in Frameworks */, 108 | 010E486C27A5BC7500D950EF /* GRDBQuery in Frameworks */, 109 | 0145B13327A5B12300E0EC43 /* GRDB in Frameworks */, 110 | ); 111 | runOnlyForDeploymentPostprocessing = 0; 112 | }; 113 | /* End PBXFrameworksBuildPhase section */ 114 | 115 | /* Begin PBXGroup section */ 116 | 0101ACF727B036350010E212 /* Commands */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 010E487527A69F0F00D950EF /* Updater */, 120 | 01C8269D27A87FD70043E890 /* CreateApnsView.swift */, 121 | 0101ACF827B036A50010E212 /* SaveApnsView.swift */, 122 | 01F8C2E727B04C4E00B37588 /* DeleteApnsView.swift */, 123 | ); 124 | path = Commands; 125 | sourceTree = ""; 126 | }; 127 | 0101ACFA27B03ECF0010E212 /* AppState */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 012D75CC27A984CC00A55457 /* AppState.swift */, 131 | 0101ACFB27B03EE50010E212 /* AppState+Renaming.swift */, 132 | 0101ACFD27B03F850010E212 /* AppState+Creation.swift */, 133 | 0101ACFF27B03FBB0010E212 /* AppState+Deletion.swift */, 134 | 0101AD0127B03FFC0010E212 /* AppState+SendPush.swift */, 135 | ); 136 | path = AppState; 137 | sourceTree = ""; 138 | }; 139 | 010E486D27A5BF6200D950EF /* ApnsList */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 010E486E27A5BF7C00D950EF /* ApnsListView.swift */, 143 | ); 144 | path = ApnsList; 145 | sourceTree = ""; 146 | }; 147 | 010E487527A69F0F00D950EF /* Updater */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 010E487627A69F1A00D950EF /* UpdaterViewModel.swift */, 151 | 010E487827A69F4900D950EF /* CheckForUpdatesView.swift */, 152 | ); 153 | path = Updater; 154 | sourceTree = ""; 155 | }; 156 | 012CEBFD27A7220900D76A5A /* Settings */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 012CEBFE27A7221900D76A5A /* SettingsView.swift */, 160 | 012CEC0027A7225000D76A5A /* GeneralSettingsView.swift */, 161 | ); 162 | path = Settings; 163 | sourceTree = ""; 164 | }; 165 | 0145B11927A592A900E0EC43 /* Models */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | 0145B11A27A592B800E0EC43 /* APNS.swift */, 169 | 013F50A527B2F9BC002B7970 /* APNS+CertificateType.swift */, 170 | 0145B12B27A5984D00E0EC43 /* APNS+IdentityType.swift */, 171 | 0145B11F27A5930800E0EC43 /* APNS+PayloadType.swift */, 172 | 0145B12D27A59A3D00E0EC43 /* APNS+Priority.swift */, 173 | 0145B12427A5950400E0EC43 /* SecIdentityType.swift */, 174 | ); 175 | path = Models; 176 | sourceTree = ""; 177 | }; 178 | 0145B12A27A597B200E0EC43 /* Sender */ = { 179 | isa = PBXGroup; 180 | children = ( 181 | 0145B12827A5975200E0EC43 /* SenderView.swift */, 182 | ); 183 | path = Sender; 184 | sourceTree = ""; 185 | }; 186 | 014D0C6627A1AECC00C43A74 = { 187 | isa = PBXGroup; 188 | children = ( 189 | 0188035227B9421100823EE6 /* Packages */, 190 | 015E27AD27B02F5F002D50B4 /* BuildTools */, 191 | 014D0C7127A1AECC00C43A74 /* Swush */, 192 | 014D0C7027A1AECC00C43A74 /* Products */, 193 | 01C8269927A87A020043E890 /* Recovered References */, 194 | 0188035427B949D900823EE6 /* Frameworks */, 195 | ); 196 | sourceTree = ""; 197 | }; 198 | 014D0C7027A1AECC00C43A74 /* Products */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 014D0C6F27A1AECC00C43A74 /* Swush.app */, 202 | ); 203 | name = Products; 204 | sourceTree = ""; 205 | }; 206 | 014D0C7127A1AECC00C43A74 /* Swush */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 01FFBC7E27A6A6C400D8780C /* Info.plist */, 210 | 014D0C8927A1D82B00C43A74 /* Sources */, 211 | 014D0C7627A1AECD00C43A74 /* Assets.xcassets */, 212 | 014D0C7B27A1AECD00C43A74 /* Swush.entitlements */, 213 | 014D0C7827A1AECD00C43A74 /* Preview Content */, 214 | ); 215 | path = Swush; 216 | sourceTree = ""; 217 | }; 218 | 014D0C7827A1AECD00C43A74 /* Preview Content */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | 014D0C7927A1AECD00C43A74 /* Preview Assets.xcassets */, 222 | ); 223 | path = "Preview Content"; 224 | sourceTree = ""; 225 | }; 226 | 014D0C8927A1D82B00C43A74 /* Sources */ = { 227 | isa = PBXGroup; 228 | children = ( 229 | 0188035727B953AD00823EE6 /* Components */, 230 | 0194C7D627A5B4EA00FB943F /* Database */, 231 | 0145B11927A592A900E0EC43 /* Models */, 232 | 014D0C8E27A1E5CF00C43A74 /* Services */, 233 | 014D0C8C27A1D84D00C43A74 /* Extensions */, 234 | 014D0C8B27A1D83800C43A74 /* App */, 235 | 014D0C8A27A1D83300C43A74 /* Modules */, 236 | ); 237 | path = Sources; 238 | sourceTree = ""; 239 | }; 240 | 014D0C8A27A1D83300C43A74 /* Modules */ = { 241 | isa = PBXGroup; 242 | children = ( 243 | 0101ACF727B036350010E212 /* Commands */, 244 | 012CEBFD27A7220900D76A5A /* Settings */, 245 | 010E486D27A5BF6200D950EF /* ApnsList */, 246 | 0145B12A27A597B200E0EC43 /* Sender */, 247 | 014D0C7427A1AECC00C43A74 /* ContentView.swift */, 248 | ); 249 | path = Modules; 250 | sourceTree = ""; 251 | }; 252 | 014D0C8B27A1D83800C43A74 /* App */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | 0101ACFA27B03ECF0010E212 /* AppState */, 256 | 014D0C7227A1AECC00C43A74 /* SwushApp.swift */, 257 | ); 258 | path = App; 259 | sourceTree = ""; 260 | }; 261 | 014D0C8C27A1D84D00C43A74 /* Extensions */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | 01C8269A27A87C450043E890 /* NSTextView+Extensions.swift */, 265 | 0145B11D27A592E700E0EC43 /* URLResponse+Extensions.swift */, 266 | 0145B12627A5973000E0EC43 /* String+Extensions.swift */, 267 | 0145B12F27A5A95200E0EC43 /* SecIdentity+Extensions.swift */, 268 | 012CEBFB27A7204400D76A5A /* Published+Extensions.swift */, 269 | 01F8C2E527B04B8300B37588 /* View+ConditionalModifier.swift */, 270 | 01C1BCC527BC2F9F0080088A /* View+NSCursor.swift */, 271 | ); 272 | path = Extensions; 273 | sourceTree = ""; 274 | }; 275 | 014D0C8E27A1E5CF00C43A74 /* Services */ = { 276 | isa = PBXGroup; 277 | children = ( 278 | 014D0C8327A1BCEC00C43A74 /* SecIdentityService.swift */, 279 | 014D0C8F27A1E61600C43A74 /* APNSService.swift */, 280 | 0188035C27B9A5CD00823EE6 /* APNSService+Error.swift */, 281 | 0145B12227A593A000E0EC43 /* DependencyProvider.swift */, 282 | ); 283 | path = Services; 284 | sourceTree = ""; 285 | }; 286 | 015E27AD27B02F5F002D50B4 /* BuildTools */ = { 287 | isa = PBXGroup; 288 | children = ( 289 | 015E27AE27B02F80002D50B4 /* Package.swift */, 290 | ); 291 | path = BuildTools; 292 | sourceTree = ""; 293 | }; 294 | 0188035227B9421100823EE6 /* Packages */ = { 295 | isa = PBXGroup; 296 | children = ( 297 | 0188035327B9422E00823EE6 /* JWT */, 298 | ); 299 | path = Packages; 300 | sourceTree = ""; 301 | }; 302 | 0188035427B949D900823EE6 /* Frameworks */ = { 303 | isa = PBXGroup; 304 | children = ( 305 | ); 306 | name = Frameworks; 307 | sourceTree = ""; 308 | }; 309 | 0188035727B953AD00823EE6 /* Components */ = { 310 | isa = PBXGroup; 311 | children = ( 312 | 0188035827B953C900823EE6 /* Input.swift */, 313 | ); 314 | path = Components; 315 | sourceTree = ""; 316 | }; 317 | 0194C7D627A5B4EA00FB943F /* Database */ = { 318 | isa = PBXGroup; 319 | children = ( 320 | 0194C7D727A5B55300FB943F /* AppDatabase.swift */, 321 | 010E486827A5BA8500D950EF /* Persistence.swift */, 322 | ); 323 | path = Database; 324 | sourceTree = ""; 325 | }; 326 | 01C8269927A87A020043E890 /* Recovered References */ = { 327 | isa = PBXGroup; 328 | children = ( 329 | 01C8269727A8798D0043E890 /* NSTextView+Extensions.swift */, 330 | ); 331 | name = "Recovered References"; 332 | sourceTree = ""; 333 | }; 334 | /* End PBXGroup section */ 335 | 336 | /* Begin PBXNativeTarget section */ 337 | 014D0C6E27A1AECC00C43A74 /* Swush */ = { 338 | isa = PBXNativeTarget; 339 | buildConfigurationList = 014D0C7E27A1AECD00C43A74 /* Build configuration list for PBXNativeTarget "Swush" */; 340 | buildPhases = ( 341 | 014D0C6B27A1AECC00C43A74 /* Sources */, 342 | 015E27B027B03074002D50B4 /* Linting */, 343 | 014D0C6C27A1AECC00C43A74 /* Frameworks */, 344 | 014D0C6D27A1AECC00C43A74 /* Resources */, 345 | ); 346 | buildRules = ( 347 | ); 348 | dependencies = ( 349 | ); 350 | name = Swush; 351 | packageProductDependencies = ( 352 | 0145B13227A5B12300E0EC43 /* GRDB */, 353 | 010E486B27A5BC7500D950EF /* GRDBQuery */, 354 | 010E487327A69EC400D950EF /* Sparkle */, 355 | 0188035527B949D900823EE6 /* JWT */, 356 | ); 357 | productName = Swush; 358 | productReference = 014D0C6F27A1AECC00C43A74 /* Swush.app */; 359 | productType = "com.apple.product-type.application"; 360 | }; 361 | /* End PBXNativeTarget section */ 362 | 363 | /* Begin PBXProject section */ 364 | 014D0C6727A1AECC00C43A74 /* Project object */ = { 365 | isa = PBXProject; 366 | attributes = { 367 | BuildIndependentTargetsInParallel = 1; 368 | LastSwiftUpdateCheck = 1320; 369 | LastUpgradeCheck = 1320; 370 | TargetAttributes = { 371 | 014D0C6E27A1AECC00C43A74 = { 372 | CreatedOnToolsVersion = 13.2.1; 373 | }; 374 | }; 375 | }; 376 | buildConfigurationList = 014D0C6A27A1AECC00C43A74 /* Build configuration list for PBXProject "Swush" */; 377 | compatibilityVersion = "Xcode 13.0"; 378 | developmentRegion = en; 379 | hasScannedForEncodings = 0; 380 | knownRegions = ( 381 | en, 382 | Base, 383 | ); 384 | mainGroup = 014D0C6627A1AECC00C43A74; 385 | packageReferences = ( 386 | 0145B13127A5B12300E0EC43 /* XCRemoteSwiftPackageReference "GRDB" */, 387 | 010E486A27A5BC7500D950EF /* XCRemoteSwiftPackageReference "GRDBQuery" */, 388 | 010E487227A69EC400D950EF /* XCRemoteSwiftPackageReference "Sparkle" */, 389 | ); 390 | productRefGroup = 014D0C7027A1AECC00C43A74 /* Products */; 391 | projectDirPath = ""; 392 | projectRoot = ""; 393 | targets = ( 394 | 014D0C6E27A1AECC00C43A74 /* Swush */, 395 | ); 396 | }; 397 | /* End PBXProject section */ 398 | 399 | /* Begin PBXResourcesBuildPhase section */ 400 | 014D0C6D27A1AECC00C43A74 /* Resources */ = { 401 | isa = PBXResourcesBuildPhase; 402 | buildActionMask = 2147483647; 403 | files = ( 404 | 014D0C7A27A1AECD00C43A74 /* Preview Assets.xcassets in Resources */, 405 | 014D0C7727A1AECD00C43A74 /* Assets.xcassets in Resources */, 406 | ); 407 | runOnlyForDeploymentPostprocessing = 0; 408 | }; 409 | /* End PBXResourcesBuildPhase section */ 410 | 411 | /* Begin PBXShellScriptBuildPhase section */ 412 | 015E27B027B03074002D50B4 /* Linting */ = { 413 | isa = PBXShellScriptBuildPhase; 414 | buildActionMask = 2147483647; 415 | files = ( 416 | ); 417 | inputFileListPaths = ( 418 | ); 419 | inputPaths = ( 420 | ); 421 | name = Linting; 422 | outputFileListPaths = ( 423 | ); 424 | outputPaths = ( 425 | ); 426 | runOnlyForDeploymentPostprocessing = 0; 427 | shellPath = /bin/sh; 428 | shellScript = "#cd BuildTools\n#SDKROOT=(xcrun --sdk macosx --show-sdk-path)\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\n#swift run -c release swiftformat --lint \"$SRCROOT\"\n"; 429 | }; 430 | /* End PBXShellScriptBuildPhase section */ 431 | 432 | /* Begin PBXSourcesBuildPhase section */ 433 | 014D0C6B27A1AECC00C43A74 /* Sources */ = { 434 | isa = PBXSourcesBuildPhase; 435 | buildActionMask = 2147483647; 436 | files = ( 437 | 0188035927B953C900823EE6 /* Input.swift in Sources */, 438 | 0145B12927A5975200E0EC43 /* SenderView.swift in Sources */, 439 | 012CEBFF27A7221900D76A5A /* SettingsView.swift in Sources */, 440 | 01F8C2E627B04B8300B37588 /* View+ConditionalModifier.swift in Sources */, 441 | 01C8269E27A87FD70043E890 /* CreateApnsView.swift in Sources */, 442 | 010E486F27A5BF7C00D950EF /* ApnsListView.swift in Sources */, 443 | 012D75CD27A984CC00A55457 /* AppState.swift in Sources */, 444 | 01F8C2E827B04C4E00B37588 /* DeleteApnsView.swift in Sources */, 445 | 0101ACFE27B03F850010E212 /* AppState+Creation.swift in Sources */, 446 | 010E487727A69F1A00D950EF /* UpdaterViewModel.swift in Sources */, 447 | 0145B11E27A592E700E0EC43 /* URLResponse+Extensions.swift in Sources */, 448 | 012CEBFC27A7204400D76A5A /* Published+Extensions.swift in Sources */, 449 | 0145B12327A593A000E0EC43 /* DependencyProvider.swift in Sources */, 450 | 0194C7D827A5B55300FB943F /* AppDatabase.swift in Sources */, 451 | 014D0C8427A1BCEC00C43A74 /* SecIdentityService.swift in Sources */, 452 | 01C1BCC627BC2F9F0080088A /* View+NSCursor.swift in Sources */, 453 | 0145B12727A5973000E0EC43 /* String+Extensions.swift in Sources */, 454 | 0101ACF927B036A50010E212 /* SaveApnsView.swift in Sources */, 455 | 0145B12527A5950400E0EC43 /* SecIdentityType.swift in Sources */, 456 | 0145B13027A5A95200E0EC43 /* SecIdentity+Extensions.swift in Sources */, 457 | 0188035D27B9A5CD00823EE6 /* APNSService+Error.swift in Sources */, 458 | 010E486927A5BA8500D950EF /* Persistence.swift in Sources */, 459 | 012CEC0127A7225000D76A5A /* GeneralSettingsView.swift in Sources */, 460 | 0145B12E27A59A3D00E0EC43 /* APNS+Priority.swift in Sources */, 461 | 014D0C9027A1E61600C43A74 /* APNSService.swift in Sources */, 462 | 013F50A627B2F9BC002B7970 /* APNS+CertificateType.swift in Sources */, 463 | 0101AD0227B03FFC0010E212 /* AppState+SendPush.swift in Sources */, 464 | 014D0C7527A1AECC00C43A74 /* ContentView.swift in Sources */, 465 | 014D0C7327A1AECC00C43A74 /* SwushApp.swift in Sources */, 466 | 0145B12C27A5984D00E0EC43 /* APNS+IdentityType.swift in Sources */, 467 | 0145B12027A5930800E0EC43 /* APNS+PayloadType.swift in Sources */, 468 | 0101ACFC27B03EE50010E212 /* AppState+Renaming.swift in Sources */, 469 | 0145B11B27A592B800E0EC43 /* APNS.swift in Sources */, 470 | 01C8269B27A87C450043E890 /* NSTextView+Extensions.swift in Sources */, 471 | 010E487927A69F4900D950EF /* CheckForUpdatesView.swift in Sources */, 472 | 0101AD0027B03FBB0010E212 /* AppState+Deletion.swift in Sources */, 473 | ); 474 | runOnlyForDeploymentPostprocessing = 0; 475 | }; 476 | /* End PBXSourcesBuildPhase section */ 477 | 478 | /* Begin XCBuildConfiguration section */ 479 | 014D0C7C27A1AECD00C43A74 /* Debug */ = { 480 | isa = XCBuildConfiguration; 481 | buildSettings = { 482 | ALWAYS_SEARCH_USER_PATHS = NO; 483 | CLANG_ANALYZER_NONNULL = YES; 484 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 485 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 486 | CLANG_CXX_LIBRARY = "libc++"; 487 | CLANG_ENABLE_MODULES = YES; 488 | CLANG_ENABLE_OBJC_ARC = YES; 489 | CLANG_ENABLE_OBJC_WEAK = YES; 490 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 491 | CLANG_WARN_BOOL_CONVERSION = YES; 492 | CLANG_WARN_COMMA = YES; 493 | CLANG_WARN_CONSTANT_CONVERSION = YES; 494 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 495 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 496 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 497 | CLANG_WARN_EMPTY_BODY = YES; 498 | CLANG_WARN_ENUM_CONVERSION = YES; 499 | CLANG_WARN_INFINITE_RECURSION = YES; 500 | CLANG_WARN_INT_CONVERSION = YES; 501 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 502 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 503 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 504 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 505 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 506 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 507 | CLANG_WARN_STRICT_PROTOTYPES = YES; 508 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 509 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 510 | CLANG_WARN_UNREACHABLE_CODE = YES; 511 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 512 | COPY_PHASE_STRIP = NO; 513 | DEBUG_INFORMATION_FORMAT = dwarf; 514 | ENABLE_STRICT_OBJC_MSGSEND = YES; 515 | ENABLE_TESTABILITY = YES; 516 | GCC_C_LANGUAGE_STANDARD = gnu11; 517 | GCC_DYNAMIC_NO_PIC = NO; 518 | GCC_NO_COMMON_BLOCKS = YES; 519 | GCC_OPTIMIZATION_LEVEL = 0; 520 | GCC_PREPROCESSOR_DEFINITIONS = ( 521 | "DEBUG=1", 522 | "$(inherited)", 523 | ); 524 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 525 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 526 | GCC_WARN_UNDECLARED_SELECTOR = YES; 527 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 528 | GCC_WARN_UNUSED_FUNCTION = YES; 529 | GCC_WARN_UNUSED_VARIABLE = YES; 530 | MACOSX_DEPLOYMENT_TARGET = 12.1; 531 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 532 | MTL_FAST_MATH = YES; 533 | ONLY_ACTIVE_ARCH = YES; 534 | SDKROOT = macosx; 535 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 536 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 537 | }; 538 | name = Debug; 539 | }; 540 | 014D0C7D27A1AECD00C43A74 /* Release */ = { 541 | isa = XCBuildConfiguration; 542 | buildSettings = { 543 | ALWAYS_SEARCH_USER_PATHS = NO; 544 | CLANG_ANALYZER_NONNULL = YES; 545 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 546 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 547 | CLANG_CXX_LIBRARY = "libc++"; 548 | CLANG_ENABLE_MODULES = YES; 549 | CLANG_ENABLE_OBJC_ARC = YES; 550 | CLANG_ENABLE_OBJC_WEAK = YES; 551 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 552 | CLANG_WARN_BOOL_CONVERSION = YES; 553 | CLANG_WARN_COMMA = YES; 554 | CLANG_WARN_CONSTANT_CONVERSION = YES; 555 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 556 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 557 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 558 | CLANG_WARN_EMPTY_BODY = YES; 559 | CLANG_WARN_ENUM_CONVERSION = YES; 560 | CLANG_WARN_INFINITE_RECURSION = YES; 561 | CLANG_WARN_INT_CONVERSION = YES; 562 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 563 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 564 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 565 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 566 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 567 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 568 | CLANG_WARN_STRICT_PROTOTYPES = YES; 569 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 570 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 571 | CLANG_WARN_UNREACHABLE_CODE = YES; 572 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 573 | COPY_PHASE_STRIP = NO; 574 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 575 | ENABLE_NS_ASSERTIONS = NO; 576 | ENABLE_STRICT_OBJC_MSGSEND = YES; 577 | GCC_C_LANGUAGE_STANDARD = gnu11; 578 | GCC_NO_COMMON_BLOCKS = YES; 579 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 580 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 581 | GCC_WARN_UNDECLARED_SELECTOR = YES; 582 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 583 | GCC_WARN_UNUSED_FUNCTION = YES; 584 | GCC_WARN_UNUSED_VARIABLE = YES; 585 | MACOSX_DEPLOYMENT_TARGET = 12.1; 586 | MTL_ENABLE_DEBUG_INFO = NO; 587 | MTL_FAST_MATH = YES; 588 | SDKROOT = macosx; 589 | SWIFT_COMPILATION_MODE = wholemodule; 590 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 591 | }; 592 | name = Release; 593 | }; 594 | 014D0C7F27A1AECD00C43A74 /* Debug */ = { 595 | isa = XCBuildConfiguration; 596 | buildSettings = { 597 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 598 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 599 | CODE_SIGN_ENTITLEMENTS = Swush/Swush.entitlements; 600 | CODE_SIGN_STYLE = Automatic; 601 | COMBINE_HIDPI_IMAGES = YES; 602 | CURRENT_PROJECT_VERSION = 8; 603 | DEVELOPMENT_ASSET_PATHS = "\"Swush/Preview Content\""; 604 | DEVELOPMENT_TEAM = 9UW6QD7CPP; 605 | ENABLE_HARDENED_RUNTIME = YES; 606 | ENABLE_PREVIEWS = YES; 607 | GENERATE_INFOPLIST_FILE = YES; 608 | INFOPLIST_FILE = Swush/Info.plist; 609 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 610 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 611 | LD_RUNPATH_SEARCH_PATHS = ( 612 | "$(inherited)", 613 | "@executable_path/../Frameworks", 614 | ); 615 | MARKETING_VERSION = 0.1.0; 616 | PRODUCT_BUNDLE_IDENTIFIER = com.qeude.Swush; 617 | PRODUCT_NAME = "$(TARGET_NAME)"; 618 | SWIFT_EMIT_LOC_STRINGS = YES; 619 | SWIFT_VERSION = 5.0; 620 | VERSIONING_SYSTEM = "apple-generic"; 621 | }; 622 | name = Debug; 623 | }; 624 | 014D0C8027A1AECD00C43A74 /* Release */ = { 625 | isa = XCBuildConfiguration; 626 | buildSettings = { 627 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 628 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 629 | CODE_SIGN_ENTITLEMENTS = Swush/Swush.entitlements; 630 | CODE_SIGN_STYLE = Automatic; 631 | COMBINE_HIDPI_IMAGES = YES; 632 | CURRENT_PROJECT_VERSION = 8; 633 | DEVELOPMENT_ASSET_PATHS = "\"Swush/Preview Content\""; 634 | DEVELOPMENT_TEAM = 9UW6QD7CPP; 635 | ENABLE_HARDENED_RUNTIME = YES; 636 | ENABLE_PREVIEWS = YES; 637 | GENERATE_INFOPLIST_FILE = YES; 638 | INFOPLIST_FILE = Swush/Info.plist; 639 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 640 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 641 | LD_RUNPATH_SEARCH_PATHS = ( 642 | "$(inherited)", 643 | "@executable_path/../Frameworks", 644 | ); 645 | MARKETING_VERSION = 0.1.0; 646 | PRODUCT_BUNDLE_IDENTIFIER = com.qeude.Swush; 647 | PRODUCT_NAME = "$(TARGET_NAME)"; 648 | SWIFT_EMIT_LOC_STRINGS = YES; 649 | SWIFT_VERSION = 5.0; 650 | VERSIONING_SYSTEM = "apple-generic"; 651 | }; 652 | name = Release; 653 | }; 654 | /* End XCBuildConfiguration section */ 655 | 656 | /* Begin XCConfigurationList section */ 657 | 014D0C6A27A1AECC00C43A74 /* Build configuration list for PBXProject "Swush" */ = { 658 | isa = XCConfigurationList; 659 | buildConfigurations = ( 660 | 014D0C7C27A1AECD00C43A74 /* Debug */, 661 | 014D0C7D27A1AECD00C43A74 /* Release */, 662 | ); 663 | defaultConfigurationIsVisible = 0; 664 | defaultConfigurationName = Release; 665 | }; 666 | 014D0C7E27A1AECD00C43A74 /* Build configuration list for PBXNativeTarget "Swush" */ = { 667 | isa = XCConfigurationList; 668 | buildConfigurations = ( 669 | 014D0C7F27A1AECD00C43A74 /* Debug */, 670 | 014D0C8027A1AECD00C43A74 /* Release */, 671 | ); 672 | defaultConfigurationIsVisible = 0; 673 | defaultConfigurationName = Release; 674 | }; 675 | /* End XCConfigurationList section */ 676 | 677 | /* Begin XCRemoteSwiftPackageReference section */ 678 | 010E486A27A5BC7500D950EF /* XCRemoteSwiftPackageReference "GRDBQuery" */ = { 679 | isa = XCRemoteSwiftPackageReference; 680 | repositoryURL = "https://github.com/groue/GRDBQuery"; 681 | requirement = { 682 | kind = upToNextMajorVersion; 683 | minimumVersion = 0.1.0; 684 | }; 685 | }; 686 | 010E487227A69EC400D950EF /* XCRemoteSwiftPackageReference "Sparkle" */ = { 687 | isa = XCRemoteSwiftPackageReference; 688 | repositoryURL = "https://github.com/sparkle-project/Sparkle"; 689 | requirement = { 690 | kind = upToNextMajorVersion; 691 | minimumVersion = 2.0.0; 692 | }; 693 | }; 694 | 0145B13127A5B12300E0EC43 /* XCRemoteSwiftPackageReference "GRDB" */ = { 695 | isa = XCRemoteSwiftPackageReference; 696 | repositoryURL = "https://github.com/groue/GRDB.swift"; 697 | requirement = { 698 | kind = exactVersion; 699 | version = 5.19.0; 700 | }; 701 | }; 702 | /* End XCRemoteSwiftPackageReference section */ 703 | 704 | /* Begin XCSwiftPackageProductDependency section */ 705 | 010E486B27A5BC7500D950EF /* GRDBQuery */ = { 706 | isa = XCSwiftPackageProductDependency; 707 | package = 010E486A27A5BC7500D950EF /* XCRemoteSwiftPackageReference "GRDBQuery" */; 708 | productName = GRDBQuery; 709 | }; 710 | 010E487327A69EC400D950EF /* Sparkle */ = { 711 | isa = XCSwiftPackageProductDependency; 712 | package = 010E487227A69EC400D950EF /* XCRemoteSwiftPackageReference "Sparkle" */; 713 | productName = Sparkle; 714 | }; 715 | 0145B13227A5B12300E0EC43 /* GRDB */ = { 716 | isa = XCSwiftPackageProductDependency; 717 | package = 0145B13127A5B12300E0EC43 /* XCRemoteSwiftPackageReference "GRDB" */; 718 | productName = GRDB; 719 | }; 720 | 0188035527B949D900823EE6 /* JWT */ = { 721 | isa = XCSwiftPackageProductDependency; 722 | productName = JWT; 723 | }; 724 | /* End XCSwiftPackageProductDependency section */ 725 | }; 726 | rootObject = 014D0C6727A1AECC00C43A74 /* Project object */; 727 | } 728 | -------------------------------------------------------------------------------- /Swush.xcodeproj/xcshareddata/xcschemes/Swush - Debug.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Swush.xcodeproj/xcshareddata/xcschemes/Swush - Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Swush/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 | -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-33.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-257.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-513.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Icon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-128.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-16.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-256.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-257.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-257.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-32.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-33.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-512.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-513.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-513.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/AppIcon.appiconset/Icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/Swush/Assets.xcassets/AppIcon.appiconset/Icon-64.png -------------------------------------------------------------------------------- /Swush/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Swush/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleShortVersionString 6 | 0.4.1 7 | CFBundleVersion 8 | 8 9 | SUFeedURL 10 | https://swush.s3.eu-west-2.amazonaws.com/appcast.xml 11 | SUPublicEDKey 12 | mYA62KxOveGobH7xIWZd/M7ulv9zMgBXnbzQ8qc+weo= 13 | 14 | 15 | -------------------------------------------------------------------------------- /Swush/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Swush/Sources/App/AppState/AppState+Creation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState+Commands.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 06/02/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension AppState { 11 | func create() async { 12 | do { 13 | var apns = APNS.new 14 | try await AppDatabase.shared.saveAPNS(&apns) 15 | selectedApns = apns 16 | startRenaming(apns) 17 | } catch { 18 | print(error) 19 | } 20 | } 21 | 22 | func save() async { 23 | do { 24 | var apns = APNS( 25 | id: selectedApns?.id, 26 | name: selectedApns?.name ?? "", 27 | creationDate: selectedApns?.creationDate ?? Date(), 28 | updateDate: Date(), 29 | certificateType: selectedCertificateType, 30 | rawPayload: payload, 31 | deviceToken: deviceToken, 32 | topic: selectedTopic, 33 | payloadType: selectedPayloadType, 34 | priority: priority, 35 | isSandbox: selectedIdentityType == .sandbox, 36 | collapseId: collapseId, 37 | notificationId: notificationId, 38 | expiration: expiration 39 | ) 40 | try await AppDatabase.shared.saveAPNS(&apns) 41 | } catch { 42 | print(error) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Swush/Sources/App/AppState/AppState+Deletion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState+Deletion.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 06/02/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension AppState { 11 | func showDeleteAlert(for apns: APNS) { 12 | apnsToDelete = apns 13 | showDeleteAlert = true 14 | } 15 | 16 | func delete(apns: APNS) async { 17 | guard let id = apns.id else { return } 18 | do { 19 | try await AppDatabase.shared.deleteAPNS(ids: [id]) 20 | apnsToDelete = nil 21 | } catch { 22 | print(error) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Swush/Sources/App/AppState/AppState+Renaming.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState+Renaming.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 06/02/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension AppState { 11 | func startRenaming(_ apns: APNS) { 12 | newName = apns.name 13 | apnsToRename = apns 14 | canCreateNewApns = false 15 | canRenameApns = false 16 | } 17 | 18 | func performRenaming() async { 19 | if let apnsToRename = apnsToRename, 20 | !newName.isEmpty 21 | { 22 | await save(apns: apnsToRename, with: newName) 23 | } 24 | apnsToRename = nil 25 | newName = "" 26 | canCreateNewApns = true 27 | canRenameApns = true 28 | } 29 | 30 | private func save(apns: APNS, with newName: String) async { 31 | do { 32 | var apns = APNS( 33 | id: apns.id, 34 | name: newName, 35 | creationDate: apns.creationDate, 36 | updateDate: Date(), 37 | certificateType: apns.certificateType, 38 | rawPayload: apns.rawPayload, 39 | deviceToken: apns.deviceToken, 40 | topic: apns.topic, 41 | payloadType: apns.payloadType, 42 | priority: apns.priority, 43 | isSandbox: apns.isSandbox, 44 | collapseId: apns.collapseId, 45 | notificationId: apns.notificationId, 46 | expiration: apns.expiration 47 | ) 48 | try await AppDatabase.shared.saveAPNS(&apns) 49 | selectedApns = apns 50 | } catch { 51 | print(error) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Swush/Sources/App/AppState/AppState+SendPush.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState+Send.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 06/02/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension AppState { 11 | func sendPush() async { 12 | guard let _ = payload.toJSON() else { 13 | errorMessage = "Please provide a valid JSON payload." 14 | showErrorMessage = true 15 | return 16 | } 17 | switch selectedCertificateType { 18 | case .p8(let filename, _, _): 19 | if !FileManager.default.fileExists(atPath: filename) { 20 | errorMessage = "Please provide a valid .p8 token." 21 | showErrorMessage = true 22 | return 23 | } 24 | case .keychain: break 25 | } 26 | let apns = APNS( 27 | name: name, 28 | creationDate: selectedApns?.creationDate ?? Date(), 29 | updateDate: selectedApns?.updateDate ?? Date(), 30 | certificateType: selectedCertificateType, 31 | rawPayload: payload, 32 | deviceToken: deviceToken, 33 | topic: selectedTopic, 34 | payloadType: selectedPayloadType, 35 | priority: priority, 36 | isSandbox: selectedIdentityType == .sandbox, 37 | collapseId: collapseId, 38 | notificationId: notificationId, 39 | expiration: expiration 40 | ) 41 | do { 42 | try await DependencyProvider.apnsService.sendPush(for: apns) 43 | } catch let error as APNSService.APIError { 44 | print(error) 45 | errorMessage = error.description 46 | showErrorMessage = true 47 | } catch { 48 | print(error) 49 | } 50 | 51 | } 52 | 53 | private func sendPushWithApnsToken() { 54 | 55 | } 56 | 57 | private func sendPushWithApnsCertificate() { 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Swush/Sources/App/AppState/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 01/02/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @MainActor 12 | class AppState: ObservableObject { 13 | enum CertificateType { 14 | case keychain, p8 15 | } 16 | 17 | // MARK: Sidebar 18 | 19 | @Published var showDeleteAlert: Bool = false 20 | @Published var apnsToDelete: APNS? = nil 21 | 22 | @Published var apnsToRename: APNS? = nil 23 | @Published var newName: String = "" 24 | 25 | @Published var canCreateNewApns: Bool = true 26 | @Published var canRenameApns: Bool = true 27 | 28 | @Published var selectedApns: APNS? = nil { 29 | didSet { 30 | if let apns = selectedApns, oldValue != selectedApns { 31 | setApns(apns) 32 | } 33 | } 34 | } 35 | 36 | // MARK: APNS form 37 | 38 | @Published var selectedCertificateType: APNS.CertificateType = .keychain(certificate: nil) { 39 | didSet { 40 | didChangeCertificateType() 41 | } 42 | } 43 | 44 | @Published var name: String = "" 45 | @Published var selectedIdentityType: APNS.IdentityType = .sandbox 46 | @Published var deviceToken = "" 47 | @Published var payload = 48 | "{\n\t\"aps\": {\n\t\t\"alert\": \"Push test!\",\n\t\t\"sound\": \"default\",\n\t}\n}" 49 | @Published var topics: [String] = [] 50 | @Published var priority: APNS.Priority = .high 51 | @Published var selectedTopic: String = "" 52 | @Published var showCertificateTypePicker: Bool = false 53 | @Published var selectedPayloadType: APNS.PayloadType = .alert 54 | @Published var showErrorMessage: Bool = false 55 | @Published var errorMessage: String = "" 56 | @Published var collapseId: String = "" 57 | @Published var notificationId: String = "" 58 | @Published var expiration: String = "" 59 | 60 | var canSendApns: Bool { 61 | return !deviceToken.isEmpty && !payload.isEmpty && !selectedTopic.isEmpty && !selectedCertificateType.isEmptyOrNil 62 | } 63 | 64 | private func setApns(_ apns: APNS) { 65 | selectedCertificateType = apns.certificateType 66 | selectedIdentityType = apns.isSandbox ? .sandbox : .production 67 | deviceToken = apns.deviceToken 68 | payload = apns.rawPayload 69 | topics = apns.topics 70 | priority = apns.priority 71 | selectedTopic = apns.topic 72 | selectedPayloadType = apns.payloadType 73 | name = apns.name 74 | didChangeCertificateType() 75 | } 76 | 77 | private func didChangeCertificateType() { 78 | switch selectedCertificateType { 79 | case .p8(let filepath, let teamId, let keyId): didChange(filepath: filepath, teamId: teamId, keyId: keyId) 80 | case .keychain(let certificate): didChange(identity: certificate) 81 | } 82 | } 83 | 84 | private func didChange(filepath: String, teamId: String, keyId: String) { 85 | showCertificateTypePicker = true 86 | } 87 | 88 | private func didChange(identity: SecIdentity?) { 89 | guard let identity = identity else { 90 | topics = [] 91 | return 92 | } 93 | let type = identity.type 94 | switch type { 95 | case .universal: 96 | showCertificateTypePicker = true 97 | case .production: 98 | selectedIdentityType = .production 99 | default: 100 | break 101 | } 102 | 103 | topics = identity.topics 104 | selectedTopic = topics.first ?? "" 105 | } 106 | 107 | func selectionBindingForId(apns: APNS?) -> Binding { 108 | Binding { () -> Bool in 109 | self.selectedApns?.id == apns?.id 110 | } set: { newValue in 111 | if newValue { 112 | self.selectedApns = apns 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Swush/Sources/App/SwushApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwushApp.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 26/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwushApp: App { 12 | @StateObject var updaterViewModel = UpdaterViewModel() 13 | @StateObject var appState = AppState() 14 | 15 | var body: some Scene { 16 | WindowGroup { 17 | ContentView() 18 | .environmentObject(appState) 19 | .environment(\.appDatabase, .shared) 20 | .onAppear { 21 | updaterViewModel.checkForUpdatesInBackground() 22 | } 23 | .environmentObject(updaterViewModel) 24 | } 25 | .commands { 26 | CommandGroup(after: .appInfo) { 27 | CheckForUpdatesView() 28 | .environmentObject(updaterViewModel) 29 | } 30 | CommandGroup(replacing: .newItem) { 31 | CreateApnsView() 32 | .environmentObject(appState) 33 | } 34 | CommandGroup(replacing: .saveItem) { 35 | SaveApnsView() 36 | .environmentObject(appState) 37 | } 38 | CommandGroup(after: .saveItem) { 39 | DeleteApnsView() 40 | .environmentObject(appState) 41 | } 42 | } 43 | 44 | #if os(macOS) 45 | Settings { 46 | SettingsView().environmentObject(updaterViewModel) 47 | } 48 | #endif 49 | } 50 | } 51 | 52 | private struct AppDatabaseKey: EnvironmentKey { 53 | static var defaultValue: AppDatabase { .empty() } 54 | } 55 | 56 | extension EnvironmentValues { 57 | var appDatabase: AppDatabase { 58 | get { self[AppDatabaseKey.self] } 59 | set { self[AppDatabaseKey.self] = newValue } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Swush/Sources/Components/Input.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Input.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 13/02/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Input: View { 11 | let label: String 12 | let help: AttributedString? 13 | let content: Content 14 | @State private var showPopover = false 15 | 16 | init(label: String, help: String? = nil, @ViewBuilder _ content: () -> Content) { 17 | self.content = content() 18 | self.label = label 19 | if let help = help { 20 | self.help = try! AttributedString(markdown: help, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)) 21 | } else { 22 | self.help = nil 23 | } 24 | } 25 | 26 | var body: some View { 27 | VStack(alignment: .leading, spacing: 6) { 28 | HStack { 29 | Text(label).bold() 30 | if let help = help { 31 | Button { 32 | showPopover.toggle() 33 | } label: { 34 | Image(systemName: "info.circle") 35 | .foregroundColor(.primary) 36 | } 37 | .buttonStyle(.borderless) 38 | .cursor(.pointingHand) 39 | .popover(isPresented: $showPopover) { 40 | Text(help).padding() 41 | } 42 | } 43 | } 44 | content 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Swush/Sources/Database/AppDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDatabase.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | import GRDB 10 | 11 | struct AppDatabase { 12 | init(_ dbWriter: DatabaseWriter) throws { 13 | self.dbWriter = dbWriter 14 | try migrator.migrate(dbWriter) 15 | } 16 | 17 | let dbWriter: DatabaseWriter 18 | 19 | private var migrator: DatabaseMigrator { 20 | var migrator = DatabaseMigrator() 21 | 22 | #if DEBUG 23 | // Speed up development by nuking the database when migrations change 24 | // See https://github.com/groue/GRDB.swift/blob/master/Documentation/Migrations.md#the-erasedatabaseonschemachange-option 25 | migrator.eraseDatabaseOnSchemaChange = true 26 | #endif 27 | 28 | migrator.registerMigration("createApns") { db in 29 | // Create a table 30 | // See https://github.com/groue/GRDB.swift#create-tables 31 | try db.create(table: "apns") { t in 32 | t.autoIncrementedPrimaryKey("id") 33 | t.column("name", .text).notNull() 34 | t.column("creationDate", .datetime).notNull() 35 | t.column("updateDate", .datetime).notNull() 36 | t.column("rawCertificateType", .text).notNull() 37 | t.column("identityString", .text) 38 | t.column("filepath", .text) 39 | t.column("teamId", .text) 40 | t.column("keyId", .text) 41 | t.column("rawPayload", .text).notNull() 42 | t.column("deviceToken", .text).notNull() 43 | t.column("topic", .text).notNull() 44 | t.column("payloadType", .text).notNull() 45 | t.column("priority", .integer).notNull() 46 | t.column("isSandbox", .boolean).notNull() 47 | t.column("collapseId", .text) 48 | t.column("notificationId", .text) 49 | t.column("expiration", .text) 50 | } 51 | } 52 | return migrator 53 | } 54 | } 55 | 56 | // MARK: - Database Access: Writes 57 | 58 | extension AppDatabase {} 59 | 60 | // MARK: - Database Access: Reads 61 | 62 | extension AppDatabase { 63 | /// Provides a read-only access to the database 64 | var databaseReader: DatabaseReader { 65 | dbWriter 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Swush/Sources/Database/Persistence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistence.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | import GRDB 10 | 11 | extension AppDatabase { 12 | static let shared = makeShared() 13 | 14 | private static func makeShared() -> AppDatabase { 15 | do { 16 | // Pick a folder for storing the SQLite database, as well as 17 | // the various temporary files created during normal database 18 | // operations (https://sqlite.org/tempfiles.html). 19 | let fileManager = FileManager() 20 | let folderURL = 21 | try fileManager 22 | .url( 23 | for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true 24 | ) 25 | .appendingPathComponent("database", isDirectory: true) 26 | 27 | // Support for tests: delete the database if requested 28 | if CommandLine.arguments.contains("-reset") { 29 | try? fileManager.removeItem(at: folderURL) 30 | } 31 | 32 | // Create the database folder if needed 33 | try fileManager.createDirectory(at: folderURL, withIntermediateDirectories: true) 34 | 35 | // Connect to a database on disk 36 | // See https://github.com/groue/GRDB.swift/blob/master/README.md#database-connections 37 | let dbURL = folderURL.appendingPathComponent("db.sqlite") 38 | let dbPool = try DatabasePool(path: dbURL.path) 39 | 40 | // Create the AppDatabase 41 | let appDatabase = try AppDatabase(dbPool) 42 | return appDatabase 43 | } catch { 44 | // Replace this implementation with code to handle the error appropriately. 45 | // fatalError() causes the application to generate a crash log and terminate. 46 | // 47 | // Typical reasons for an error here include: 48 | // * The parent directory cannot be created, or disallows writing. 49 | // * The database is not accessible, due to permissions or data protection when the device is locked. 50 | // * The device is out of space. 51 | // * The database could not be migrated to its latest schema version. 52 | // Check the error message to determine what the actual problem was. 53 | fatalError("Unresolved error \(error)") 54 | } 55 | } 56 | 57 | /// Creates an empty database for SwiftUI previews 58 | static func empty() -> AppDatabase { 59 | // Connect to an in-memory database 60 | // See https://github.com/groue/GRDB.swift/blob/master/README.md#database-connections 61 | let dbQueue = DatabaseQueue() 62 | return try! AppDatabase(dbQueue) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Swush/Sources/Extensions/NSTextView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextView+Extensions.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 31/01/2022. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSTextView { 11 | override open var frame: CGRect { 12 | didSet { 13 | isAutomaticQuoteSubstitutionEnabled = false 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Swush/Sources/Extensions/Published+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Published+Extensions.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 30/01/2022. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | private var cancellables = [String: AnyCancellable]() 12 | 13 | public extension Published { 14 | init(wrappedValue defaultValue: Value, key: String) { 15 | let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue 16 | self.init(initialValue: value) 17 | cancellables[key] = projectedValue.sink { val in 18 | UserDefaults.standard.set(val, forKey: key) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Swush/Sources/Extensions/SecIdentity+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecIdentity+Extensions.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | import SecurityInterface 10 | 11 | extension SecIdentity { 12 | var type: SecIdentityType { 13 | let values = values 14 | if values?[SecIdentityType.sandbox.rawValue] != nil, 15 | values?[SecIdentityType.production.rawValue] != nil 16 | { 17 | return .universal 18 | } else if values?[SecIdentityType.sandbox.rawValue] != nil { 19 | return .sandbox 20 | } else if values?[SecIdentityType.production.rawValue] != nil { 21 | return .production 22 | } else { 23 | return .invalid 24 | } 25 | } 26 | 27 | var values: [String: AnyObject]? { 28 | var certificate: SecCertificate? 29 | SecIdentityCopyCertificate(self, &certificate) 30 | let keys = [ 31 | SecIdentityType.sandbox.rawValue, 32 | SecIdentityType.production.rawValue, 33 | SecIdentityType.universal.rawValue, 34 | kSecOIDInvalidityDate as String, 35 | ] 36 | let values = SecCertificateCopyValues(certificate!, keys as CFArray, nil) 37 | 38 | certificate = nil 39 | return values as? [String: AnyObject] 40 | } 41 | 42 | var topics: [String] { 43 | let values = values 44 | if values?[SecIdentityType.sandbox.rawValue] != nil, 45 | values?[SecIdentityType.production.rawValue] != nil 46 | { 47 | if let topicContents = values?[SecIdentityType.universal.rawValue] { 48 | let topicArray: [[String: Any]] = topicContents["value"] as? [[String: Any]] ?? [] 49 | return topicArray.compactMap { topic in 50 | if topic["label"] as? String == "Data" { 51 | return topic["value"] as? String 52 | } 53 | return nil 54 | } 55 | } 56 | } 57 | return [] 58 | } 59 | 60 | var expiryDate: Date? { 61 | let values = values 62 | if values?[kSecOIDInvalidityDate as String] != nil { 63 | if let content = values?[kSecOIDInvalidityDate as String] { 64 | return content["value"] as? Date 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | var name: String? { 71 | var certificate: SecCertificate? 72 | var name: CFString? 73 | SecIdentityCopyCertificate(self, &certificate) 74 | SecCertificateCopyCommonName(certificate!, &name) 75 | guard let name = name else { return nil } 76 | return name as String 77 | } 78 | 79 | var humanReadable: String { 80 | var dateString = "" 81 | if let expiryDate = expiryDate { 82 | let formatter = DateFormatter() 83 | formatter.dateStyle = .short 84 | formatter.timeStyle = .short 85 | dateString = formatter.string(from: expiryDate) 86 | } 87 | return "🎫 \(name ?? "") (\(SecIdentityType.formattedString(for: type))) - 🚮 \(dateString)" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Swush/Sources/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func toJSON() -> [String: Any]? { 12 | guard let data = data(using: .utf8, allowLossyConversion: false) else { return nil } 13 | return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) 14 | as? [String: Any] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Swush/Sources/Extensions/URLResponse+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLResponse+Extensions.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLResponse { 11 | var status: Int? { 12 | if let httpResponse = self as? HTTPURLResponse { 13 | return httpResponse.statusCode 14 | } 15 | return nil 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Swush/Sources/Extensions/View+ConditionalModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+ConditionalModifier.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 06/02/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension View { 12 | /// Applies the given transform if the given condition evaluates to `true`. 13 | /// - Parameters: 14 | /// - condition: The condition to evaluate. 15 | /// - transform: The transform to apply to the source `View`. 16 | /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. 17 | @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { 18 | if condition { 19 | transform(self) 20 | } else { 21 | self 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Swush/Sources/Extensions/View+NSCursor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+NSCursor.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 15/02/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | /// https://stackoverflow.com/a/61985678/3393964 12 | public func cursor(_ cursor: NSCursor) -> some View { 13 | self.onHover { inside in 14 | if inside { 15 | cursor.push() 16 | } else { 17 | NSCursor.pop() 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Swush/Sources/Models/APNS+CertificateType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APNS+CertificateType.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 08/02/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension APNS { 11 | enum CertificateType: CaseIterable, Hashable { 12 | case keychain(certificate: SecIdentity?) 13 | case p8(filepath: String, teamId: String, keyId: String) 14 | 15 | static var allCases: [APNS.CertificateType] = [.keychain(certificate: nil), .p8(filepath: "", teamId: "", keyId: "")] 16 | static var allRawCases: [String] = allCases.map { $0.rawValue } 17 | 18 | var isEmptyOrNil: Bool { 19 | switch self { 20 | case .keychain(let certificate): return certificate == nil 21 | case .p8(let filepath, let teamId, let keyId): return filepath.isEmpty || teamId.isEmpty || keyId.isEmpty 22 | } 23 | } 24 | 25 | var rawValue: String { 26 | switch self { 27 | case .keychain: return "keychain" 28 | case .p8: return "p8" 29 | } 30 | } 31 | 32 | static func placeholder(for rawValue: String) -> String { 33 | switch rawValue { 34 | case "keychain" : return "🎫 Certificate" 35 | case "p8": return "🔑 Key" 36 | default: return "" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Swush/Sources/Models/APNS+IdentityType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APNS+CertificateType.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension APNS { 11 | enum IdentityType: CaseIterable { 12 | case sandbox 13 | case production 14 | 15 | private static let mapping: [IdentityType: String] = [ 16 | .sandbox: "🏖 Sandbox", 17 | .production: "🚨 Production", 18 | ] 19 | 20 | private static let slugMapping: [IdentityType: String] = [ 21 | .sandbox: "sandbox", 22 | .production: "production", 23 | ] 24 | 25 | var slug: String { 26 | IdentityType.slugMapping[self] ?? "" 27 | } 28 | 29 | static func from(value: IdentityType) -> String { 30 | mapping[value] ?? "Unknown" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Swush/Sources/Models/APNS+PayloadType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APNS+PayloadType.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension APNS { 11 | enum PayloadType: String, Codable, CaseIterable { 12 | case alert 13 | case background 14 | case voip 15 | case complication 16 | case fileprovider 17 | case mdm 18 | 19 | private static let mapping: [PayloadType: String] = [ 20 | .alert: "Alert", 21 | .background: "Background", 22 | .voip: "Voip", 23 | .complication: "Complication", 24 | .fileprovider: "File Provider", 25 | .mdm: "Mdm", 26 | ] 27 | 28 | static func from(value: PayloadType) -> String { 29 | mapping[value] ?? "Unknown" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Swush/Sources/Models/APNS+Priority.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APNS+Priority.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension APNS { 11 | enum Priority: Int, Codable, CaseIterable { 12 | case low = 5 13 | case high = 10 14 | 15 | private static var mapping: [Priority: String] = [ 16 | .low: "🐢 Normal", 17 | .high: "⚡️ Immmediately" 18 | ] 19 | 20 | var placeholder: String { 21 | return APNS.Priority.mapping[self] ?? "Unknown" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Swush/Sources/Models/APNS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APNS.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import GRDB 11 | import GRDBQuery 12 | import Security 13 | import JWT 14 | 15 | struct APNS: Identifiable, Hashable { 16 | var id: Int64? 17 | var name: String 18 | let creationDate: Date 19 | let updateDate: Date 20 | let rawCertificateType: String 21 | let identityString: String? 22 | let filepath: String? 23 | let teamId: String? 24 | let keyId: String? 25 | let rawPayload: String 26 | let deviceToken: String 27 | let collapseId: String 28 | let notificationId: String 29 | let expiration: String 30 | let topic: String 31 | let payloadType: PayloadType 32 | let priority: Priority 33 | let isSandbox: Bool 34 | 35 | init(id: Int64? = nil, 36 | name: String, 37 | creationDate: Date, 38 | updateDate: Date, 39 | certificateType: APNS.CertificateType, 40 | rawPayload: String, 41 | deviceToken: String, 42 | topic: String, 43 | payloadType: PayloadType, 44 | priority: Priority, 45 | isSandbox: Bool, 46 | collapseId: String, 47 | notificationId: String, 48 | expiration: String 49 | ) { 50 | self.id = id 51 | self.name = name 52 | self.creationDate = creationDate 53 | self.updateDate = updateDate 54 | self.rawCertificateType = certificateType.rawValue 55 | switch certificateType { 56 | case .p8(let tokenFilename, let teamId, let keyId): 57 | self.identityString = nil 58 | self.filepath = tokenFilename 59 | self.teamId = teamId 60 | self.keyId = keyId 61 | case .keychain(let certificate): 62 | self.identityString = certificate?.humanReadable 63 | self.filepath = nil 64 | self.teamId = nil 65 | self.keyId = nil 66 | } 67 | self.rawPayload = rawPayload 68 | self.deviceToken = deviceToken 69 | self.topic = topic 70 | self.payloadType = payloadType 71 | self.priority = priority 72 | self.isSandbox = isSandbox 73 | self.collapseId = collapseId 74 | self.notificationId = notificationId 75 | self.expiration = expiration 76 | } 77 | 78 | var certificateType: CertificateType { 79 | switch rawCertificateType { 80 | case "keychain": return .keychain(certificate: identity) 81 | case "p8": return .p8(filepath: filepath ?? "", teamId: teamId ?? "", keyId: keyId ?? "") 82 | default: 83 | fatalError("Unknown certificate type") 84 | } 85 | } 86 | 87 | var payload: [String: Any]? { 88 | rawPayload.toJSON() 89 | } 90 | 91 | var topics: [String] { 92 | if case .keychain(.some(_)) = certificateType { 93 | return identity?.topics ?? [] 94 | } 95 | return [] 96 | } 97 | 98 | var jwt: String { 99 | guard let teamId = teamId, let keyId = keyId, let filepath = filepath else { fatalError() } 100 | let jwt = JWT(teamId: teamId, topic: topic, keyId: keyId, tokenFilename: filepath) 101 | return jwt.token 102 | } 103 | 104 | private var identity: SecIdentity? { 105 | DependencyProvider.secIdentityService.identities?.first(where: { 106 | $0.humanReadable == identityString 107 | }) 108 | } 109 | 110 | static var new = APNS( 111 | name: "Untitled", 112 | creationDate: Date(), 113 | updateDate: Date(), 114 | certificateType: .keychain(certificate: nil), 115 | rawPayload: 116 | "{\n\t\"aps\": {\n\t\t\"alert\": \"Push test!\",\n\t\t\"sound\": \"default\",\n\t}\n}", 117 | deviceToken: "", 118 | topic: "", 119 | payloadType: .alert, 120 | priority: .high, 121 | isSandbox: true, 122 | collapseId: "", 123 | notificationId: "", 124 | expiration: "" 125 | ) 126 | } 127 | 128 | extension APNS: Codable, FetchableRecord, MutablePersistableRecord { 129 | // Define database columns from CodingKeys 130 | fileprivate enum Columns { 131 | static let name = Column(CodingKeys.name) 132 | static let creationDate = Column(CodingKeys.creationDate) 133 | static let updateDate = Column(CodingKeys.updateDate) 134 | static let rawCertificateType = Column(CodingKeys.rawCertificateType) 135 | static let identityString = Column(CodingKeys.identityString) 136 | static let filepath = Column(CodingKeys.filepath) 137 | static let teamId = Column(CodingKeys.teamId) 138 | static let keyId = Column(CodingKeys.keyId) 139 | static let rawPayload = Column(CodingKeys.rawPayload) 140 | static let deviceToken = Column(CodingKeys.deviceToken) 141 | static let topic = Column(CodingKeys.topic) 142 | static let payloadType = Column(CodingKeys.payloadType) 143 | static let priority = Column(CodingKeys.priority) 144 | static let isSandbox = Column(CodingKeys.isSandbox) 145 | static let collapseId = Column(CodingKeys.collapseId) 146 | static let notificationId = Column(CodingKeys.notificationId) 147 | static let expiration = Column(CodingKeys.expiration) 148 | } 149 | 150 | mutating func didInsert(with rowId: Int64, for _: String?) { 151 | id = rowId 152 | } 153 | } 154 | 155 | extension AppDatabase { 156 | func saveAPNS(_ apns: inout APNS) async throws { 157 | apns = try await dbWriter.write { [apns] db in 158 | try apns.saved(db) 159 | } 160 | } 161 | 162 | func deleteAPNS(ids: [Int64]) async throws { 163 | try await dbWriter.write { db in 164 | _ = try APNS.deleteAll(db, ids: ids) 165 | } 166 | } 167 | } 168 | 169 | // MARK: - Player Database Requests 170 | 171 | /// Define some player requests used by the application. 172 | /// 173 | /// See 174 | /// See 175 | extension DerivableRequest where RowDecoder == APNS { 176 | /// A request of players ordered by name. 177 | /// 178 | /// For example: 179 | /// 180 | /// let players: [Player] = try dbWriter.read { db in 181 | /// try Player.all().orderedByName().fetchAll(db) 182 | /// } 183 | func orderedByName() -> Self { 184 | // Sort by name in a localized case insensitive fashion 185 | // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison 186 | order(APNS.Columns.name.collating(.localizedCaseInsensitiveCompare)) 187 | } 188 | 189 | /// A request of players ordered by score. 190 | /// 191 | /// For example: 192 | /// 193 | /// let players: [Player] = try dbWriter.read { db in 194 | /// try Player.all().orderedByScore().fetchAll(db) 195 | /// } 196 | /// let bestPlayer: Player? = try dbWriter.read { db in 197 | /// try Player.all().orderedByScore().fetchOne(db) 198 | /// } 199 | func orderedByCreationDate() -> Self { 200 | // Sort by descending score, and then by name, in a 201 | // localized case insensitive fashion 202 | // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison 203 | order( 204 | APNS.Columns.creationDate.desc, 205 | APNS.Columns.name.collating(.localizedCaseInsensitiveCompare) 206 | ) 207 | } 208 | 209 | func orderedByUpdateDate() -> Self { 210 | // Sort by descending score, and then by name, in a 211 | // localized case insensitive fashion 212 | // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison 213 | order( 214 | APNS.Columns.updateDate.desc, 215 | APNS.Columns.name.collating(.localizedCaseInsensitiveCompare) 216 | ) 217 | } 218 | } 219 | 220 | struct APNSRequest: Queryable { 221 | enum Ordering { 222 | case byName 223 | case byCreationDate 224 | case byUpdateDate 225 | } 226 | 227 | var ordering: Ordering 228 | 229 | static var defaultValue: [APNS] { [] } 230 | 231 | func publisher(in appDatabase: AppDatabase) -> AnyPublisher<[APNS], Error> { 232 | // Build the publisher from the general-purpose read-only access 233 | // granted by `appDatabase.databaseReader`. 234 | // Some apps will prefer to call a dedicated method of `appDatabase`. 235 | ValueObservation 236 | .tracking(fetchValue(_:)) 237 | .publisher( 238 | in: appDatabase.databaseReader, 239 | // The `.immediate` scheduling feeds the view right on 240 | // subscription, and avoids an undesired animation when the 241 | // application starts. 242 | scheduling: .immediate 243 | ) 244 | .eraseToAnyPublisher() 245 | } 246 | 247 | // This method is not required by Queryable, but it makes it easier 248 | // to test PlayerRequest. 249 | func fetchValue(_ db: Database) throws -> [APNS] { 250 | switch ordering { 251 | case .byName: 252 | return try APNS.all().orderedByName().fetchAll(db) 253 | case .byCreationDate: 254 | return try APNS.all().orderedByCreationDate().fetchAll(db) 255 | case .byUpdateDate: 256 | return try APNS.all().orderedByUpdateDate().fetchAll(db) 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /Swush/Sources/Models/SecIdentityType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecIdentityType.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | // http://www.apple.com/certificateauthority/Apple_WWDR_CPS 11 | enum SecIdentityType: String { 12 | case invalid 13 | case sandbox = "1.2.840.113635.100.6.3.1" 14 | case production = "1.2.840.113635.100.6.3.2" 15 | case universal = "1.2.840.113635.100.6.3.6" 16 | 17 | private static let mapping: [SecIdentityType: String] = [ 18 | .invalid: "Invalid", 19 | .sandbox: "Sandbox", 20 | .production: "Production", 21 | .universal: "Sandbox & Production", 22 | ] 23 | 24 | static func formattedString(for value: SecIdentityType) -> String { 25 | mapping[value] ?? "Unknown" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/ApnsList/ApnsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApnsList.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import GRDBQuery 9 | import SwiftUI 10 | 11 | struct ApnsListView: View { 12 | @Environment(\.appDatabase) private var appDatabase 13 | @EnvironmentObject private var appState: AppState 14 | 15 | @Query(APNSRequest(ordering: .byName), in: \.appDatabase) private var apnsList: [APNS] 16 | @State private var searchText: String = "" 17 | 18 | @FocusState private var isFocused: Bool 19 | 20 | private var filteredApnsList: [APNS] { 21 | if searchText.isEmpty { 22 | return apnsList 23 | } else { 24 | return apnsList.filter { $0.name.localizedCaseInsensitiveContains(searchText) } 25 | } 26 | } 27 | 28 | var body: some View { 29 | List(filteredApnsList) { apns in 30 | NavigationLink( 31 | destination: SenderView(), 32 | isActive: appState.selectionBindingForId(apns: apns) 33 | ) { 34 | if appState.apnsToRename?.id == apns.id { 35 | TextField( 36 | apns.name, 37 | text: $appState.newName, 38 | onEditingChanged: { editingChanged in 39 | if !editingChanged { 40 | Task { 41 | await appState.performRenaming() 42 | } 43 | } 44 | } 45 | ) 46 | .onDisappear { 47 | isFocused = false 48 | } 49 | .onAppear { 50 | isFocused = true 51 | } 52 | .focused($isFocused) 53 | } else { 54 | Text(apns.name) 55 | } 56 | } 57 | .frame(height: 30) 58 | .if(appState.selectedApns == apns) { view in 59 | view.onTapGesture(count: 2) { 60 | appState.startRenaming(apns) 61 | } 62 | } 63 | .contextMenu { 64 | Button { 65 | appState.startRenaming(apns) 66 | } label: { 67 | Text("Rename") 68 | } 69 | .disabled(!appState.canRenameApns) 70 | Button { 71 | appState.showDeleteAlert(for: apns) 72 | } label: { 73 | Text("Delete") 74 | } 75 | .keyboardShortcut(.delete, modifiers: [.command]) 76 | } 77 | .alert( 78 | "Do you really want to delete the APNS named \"\(appState.apnsToDelete?.name ?? "")\"? ", 79 | isPresented: $appState.showDeleteAlert 80 | ) { 81 | Button("Yes") { 82 | Task { 83 | guard let apnsToDelete = appState.apnsToDelete else { return } 84 | await appState.delete(apns: apnsToDelete) 85 | } 86 | } 87 | Button("No") {} 88 | } 89 | } 90 | .searchable(text: $searchText, placement: .sidebar) 91 | .listStyle(SidebarListStyle()) 92 | } 93 | } 94 | 95 | struct ApnsListView_Previews: PreviewProvider { 96 | static var previews: some View { 97 | ApnsListView() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/Commands/CreateApnsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateApnsCommandView.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 31/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CreateApnsView: View { 11 | @EnvironmentObject var appState: AppState 12 | 13 | var body: some View { 14 | Button { 15 | Task { 16 | await appState.create() 17 | } 18 | } label: { 19 | Text("New APNs") 20 | } 21 | .keyboardShortcut("n", modifiers: [.command]) 22 | .disabled(!appState.canCreateNewApns) 23 | } 24 | } 25 | 26 | struct CreateApnsView_Previews: PreviewProvider { 27 | static var previews: some View { 28 | CreateApnsView() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/Commands/DeleteApnsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteApnsView.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 06/02/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DeleteApnsView: View { 11 | @EnvironmentObject var appState: AppState 12 | 13 | var body: some View { 14 | Button { 15 | guard let apns = appState.selectedApns else { return } 16 | Task { 17 | appState.showDeleteAlert(for: apns) 18 | } 19 | } label: { 20 | Text("Delete APNs") 21 | } 22 | .keyboardShortcut(.delete, modifiers: [.command]) 23 | .disabled(appState.selectedApns == nil) 24 | } 25 | } 26 | 27 | struct DeleteApnsView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | DeleteApnsView() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/Commands/SaveApnsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SaveApnsView.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 06/02/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SaveApnsView: View { 11 | @EnvironmentObject var appState: AppState 12 | 13 | var body: some View { 14 | Button { 15 | Task { 16 | await appState.save() 17 | } 18 | } label: { 19 | Text("Save APNs") 20 | } 21 | .keyboardShortcut("s", modifiers: [.command]) 22 | .disabled(appState.selectedApns == nil) 23 | } 24 | } 25 | 26 | struct SaveApnsView_Previews: PreviewProvider { 27 | static var previews: some View { 28 | SaveApnsView() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/Commands/Updater/CheckForUpdatesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckForUpdatesView.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 30/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CheckForUpdatesView: View { 11 | @EnvironmentObject var updaterViewModel: UpdaterViewModel 12 | 13 | var body: some View { 14 | Button("Check for Updates…", action: updaterViewModel.checkForUpdates) 15 | .disabled(!updaterViewModel.canCheckForUpdates) 16 | } 17 | } 18 | 19 | struct CheckForUpdatesView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | CheckForUpdatesView() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/Commands/Updater/UpdaterViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdaterViewModel.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 30/01/2022. 6 | // 7 | 8 | import Foundation 9 | import Sparkle 10 | 11 | final class UpdaterViewModel: ObservableObject { 12 | private let updaterController: SPUStandardUpdaterController 13 | 14 | @Published var canCheckForUpdates = false 15 | @Published(key: "automaticallyChecksForUpdates") var automaticallyChecksForUpdates = false 16 | 17 | init() { 18 | // If you want to start the updater manually, pass false to startingUpdater and call .startUpdater() later 19 | // This is where you can also pass an updater delegate if you need one 20 | updaterController = SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil) 21 | 22 | automaticallyChecksForUpdates = updaterController.updater.automaticallyChecksForUpdates 23 | updaterController.updater.publisher(for: \.canCheckForUpdates) 24 | .assign(to: &$canCheckForUpdates) 25 | } 26 | 27 | func checkForUpdates() { 28 | updaterController.checkForUpdates(nil) 29 | } 30 | 31 | func checkForUpdatesInBackground() { 32 | if automaticallyChecksForUpdates { 33 | updaterController.updater.checkForUpdatesInBackground() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 26/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @Environment(\.appDatabase) private var appDatabase 12 | @EnvironmentObject var appState: AppState 13 | 14 | var body: some View { 15 | NavigationView { 16 | ApnsListView() 17 | Text("Create your first APNs to start using the app. 🚀") 18 | } 19 | .toolbar { 20 | ToolbarItem(placement: .navigation) { 21 | Button( 22 | action: toggleSidebar, 23 | label: { 24 | Image(systemName: "sidebar.leading") 25 | } 26 | ) 27 | } 28 | ToolbarItem(placement: .navigation) { 29 | Button { 30 | Task { 31 | await appState.create() 32 | } 33 | } label: { 34 | Image(systemName: "plus") 35 | } 36 | } 37 | ToolbarItem(placement: .primaryAction) { 38 | Button { 39 | Task { 40 | await appState.sendPush() 41 | } 42 | } label: { 43 | Image(systemName: "paperplane") 44 | } 45 | .keyboardShortcut(.return, modifiers: [.command]) 46 | .disabled(!appState.canSendApns) 47 | } 48 | } 49 | } 50 | 51 | private func toggleSidebar() { 52 | NSApp.keyWindow?.firstResponder?.tryToPerform( 53 | #selector(NSSplitViewController.toggleSidebar(_:)), with: nil 54 | ) 55 | } 56 | } 57 | 58 | struct ContentView_Previews: PreviewProvider { 59 | static var previews: some View { 60 | ContentView() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/Sender/SenderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SenderView.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import SecurityInterface 9 | import SwiftUI 10 | import Foundation 11 | import UniformTypeIdentifiers 12 | 13 | struct SenderView: View { 14 | @Environment(\.appDatabase) private var appDatabase 15 | @EnvironmentObject private var appState: AppState 16 | 17 | @State private var selectedIdentity: SecIdentity? = nil 18 | @State private var filepath: String = "" 19 | @State private var teamId: String = "" 20 | @State private var keyId: String = "" 21 | @State private var selectedRawCertificateType: String = APNS.CertificateType.keychain(certificate: nil).rawValue 22 | 23 | var body: some View { 24 | ScrollView { 25 | VStack(alignment: .leading, spacing: 38) { 26 | authenticationForm 27 | if !appState.selectedCertificateType.isEmptyOrNil { 28 | configForm 29 | optionalForm 30 | payloadForm 31 | } 32 | } 33 | .onChange(of: selectedIdentity, perform: { newValue in 34 | appState.selectedCertificateType = .keychain(certificate: selectedIdentity) 35 | }) 36 | .onChange(of: filepath, perform: { newValue in 37 | appState.selectedCertificateType = .p8(filepath: filepath, teamId: teamId, keyId: keyId) 38 | }) 39 | .onChange(of: teamId, perform: { newValue in 40 | appState.selectedCertificateType = .p8(filepath: filepath, teamId: teamId, keyId: keyId) 41 | }) 42 | .onChange(of: keyId, perform: { newValue in 43 | appState.selectedCertificateType = .p8(filepath: filepath, teamId: teamId, keyId: keyId) 44 | }) 45 | .onChange(of: selectedRawCertificateType, perform: { newValue in 46 | switch newValue { 47 | case "keychain": appState.selectedCertificateType = .keychain(certificate: selectedIdentity) 48 | case "p8": appState.selectedCertificateType = .p8(filepath: filepath, teamId: teamId, keyId: keyId) 49 | default: fatalError() 50 | } 51 | }) 52 | .onAppear { 53 | self.setup() 54 | } 55 | .alert(Text("An error occured!"), isPresented: $appState.showErrorMessage, actions: {}, message: { 56 | Text(appState.errorMessage) 57 | }) 58 | .navigationTitle(appState.name) 59 | .animation(.default, value: appState.selectedCertificateType) 60 | .padding(20) 61 | } 62 | .frame(minWidth: 350, minHeight: 350) 63 | } 64 | 65 | private var authenticationForm: some View { 66 | VStack(alignment: .leading, spacing: 16) { 67 | Text("Authentication").font(.title).bold() 68 | Picker( 69 | selection: $selectedRawCertificateType, 70 | content: { 71 | ForEach(APNS.CertificateType.allRawCases, id: \.self) { 72 | Text(APNS.CertificateType.placeholder(for: $0)) 73 | } 74 | }, 75 | label: {}) 76 | .pickerStyle(.segmented) 77 | .fixedSize() 78 | switch appState.selectedCertificateType { 79 | case .keychain: certificateAuthenticationForm 80 | case .p8: keyAuthenticationForm 81 | } 82 | } 83 | } 84 | 85 | private var certificateAuthenticationForm: some View { 86 | Input(label: "Certificate", help: "Certificates are retrieved from your Keychain. \n⚠️ If nothing appears here, it means that you have no APNs certificate stored in your keychain.\n\nYou need to retrieve it from [Certificates, Identifiers & Profiles → Certificates](https://developer.apple.com/account/resources/certificates/list) and add it to your Keychain by double-clicking on it.") { 87 | Picker(selection: $selectedIdentity, content: { 88 | Text("Select a push certificate...").tag(nil as SecIdentity?) 89 | ForEach(DependencyProvider.secIdentityService.identities ?? [], id: \.self) { 90 | Text($0.humanReadable).tag($0 as SecIdentity?) 91 | } 92 | }, label: {}) 93 | } 94 | } 95 | 96 | private var keyAuthenticationForm: some View { 97 | Group { 98 | Input(label: "Key file", help: "The `.p8` file corresponding to your key. \n\nAvailable at [Certificates, Identifiers & Profiles → Keys](https://developer.apple.com/account/resources/authkeys/list).") { 99 | HStack { 100 | Button { 101 | let filePath = showOpenPanel() 102 | self.filepath = filePath?.path ?? "" 103 | } label: { 104 | Text("Select .p8 file") 105 | } 106 | Text(self.filepath.split(separator: "/").last ?? "") 107 | } 108 | } 109 | Input(label: "Team id", help: "The Team ID of your Apple Developer Account. \n\nAvailable at [Membership](https://developer.apple.com/account/#!/membership/).") { 110 | TextField(text: $teamId, prompt: Text("Paste your team id here ..."), label: {}) 111 | .textFieldStyle(.roundedBorder).onChange(of: teamId) { newValue in 112 | teamId = newValue.trimmingCharacters(in: .whitespacesAndNewlines) 113 | } 114 | } 115 | Input(label: "Key id", help: "The key id associated to the selected `.p8` file. \n\nAvailable at [Certificates, Identifiers & Profiles → Keys](https://developer.apple.com/account/resources/authkeys/list).") { 116 | TextField(text: $keyId, prompt: Text("Paste your key id here ..."), label: {}) 117 | .textFieldStyle(.roundedBorder).onChange(of: keyId) { newValue in 118 | keyId = newValue.trimmingCharacters(in: .whitespacesAndNewlines) 119 | } 120 | } 121 | } 122 | } 123 | 124 | private func showOpenPanel() -> URL? { 125 | let openPanel = NSOpenPanel() 126 | openPanel.allowedContentTypes = [UTType(filenameExtension: "p8")!] 127 | openPanel.allowsMultipleSelection = false 128 | openPanel.canChooseDirectories = false 129 | openPanel.canChooseFiles = true 130 | let response = openPanel.runModal() 131 | return response == .OK ? openPanel.url : nil 132 | } 133 | 134 | private var configForm: some View { 135 | VStack(alignment: .leading, spacing: 16) { 136 | Text("Configuration").font(.title).bold() 137 | Input(label: "Device push token", help: "The device token for the user's device. \nYour app receives this device token when registering for remote notifications.") { 138 | TextField(text: $appState.deviceToken, prompt: Text("Enter your device push token here..."), label: {}) 139 | .textFieldStyle(.roundedBorder).onChange(of: appState.deviceToken) { newValue in 140 | appState.deviceToken = newValue.trimmingCharacters(in: .whitespacesAndNewlines) 141 | } 142 | } 143 | if appState.showCertificateTypePicker { 144 | Input(label: "Environment", help: "APNs server to use to send your notification. \n- **sandbox**: for apps signed with iOS Development Certificate, mostly apps in debug mode. \n- **production**: for apps signed with iOS Distribution Certificate, mostly apps in release mode.") { 145 | Picker( 146 | selection: $appState.selectedIdentityType, 147 | content: { 148 | ForEach(APNS.IdentityType.allCases, id: \.self) { 149 | Text(APNS.IdentityType.from(value: $0)) 150 | } 151 | }, 152 | label: {}) 153 | .pickerStyle(.segmented) 154 | .fixedSize() 155 | } 156 | } 157 | Input(label: "Push type", help: "The value of this header must accurately reflect the contents of your notification's payload. \nThe apns-push-type header field has six valid values: \n- **alert**: Use the alert push type for notifications that trigger a user interaction--for example, an alert, badge, or sound \n- **background**: Use the background push type for notifications that deliver content in the background, and don't trigger any user interactions. \n- **voip**: Use the voip push type for notifications that provide information about an incoming Voice-over-IP (VolP) call. \n- **complication**: Use the complication push type for notifications that contain update information for a watchOS app's complications \n- **fileprovider**: Use the fileprovider push type to signal changes to a File Provider extension \n- **mdm**: Use the mdm push type for notifications that tell managed devices to contact the MDM server") { 158 | Picker( 159 | selection: $appState.selectedPayloadType, 160 | content: { 161 | ForEach(APNS.PayloadType.allCases, id: \.self) { 162 | Text(APNS.PayloadType.from(value: $0)) 163 | } 164 | }, 165 | label: {}) 166 | } 167 | Input(label: "Priority", help: "The priority of the notification.\n- Specify 10 to send the notification immediately.\n- Specify 5 to send the notification based on power considerations on the user's device.\n\nFor background notifications, using \"⚡️Immediately\" is an error.") { 168 | Picker( 169 | selection: $appState.priority, 170 | content: { 171 | ForEach(APNS.Priority.allCases, id: \.self) { 172 | Text($0.placeholder) 173 | } 174 | }, 175 | label: {}) 176 | .pickerStyle(.segmented) 177 | .fixedSize() 178 | } 179 | topicForm 180 | } 181 | .animation(.default, value: appState.showCertificateTypePicker) 182 | } 183 | 184 | private var optionalForm: some View { 185 | VStack(alignment: .leading, spacing: 16) { 186 | Text("Optional").font(.title).bold() 187 | Input(label: "Collapse id", help: "An identifier you use to coalesce multiple notifications into a single notification for the user. \nTypically, each notification request causes a new notification to be displayed on the user's device. \nWhen sending the same notification more than once, use the same value in this header to coalesce the requests. \nThe value of this key must not exceed 64 bytes.") { 188 | TextField(text: $appState.collapseId, prompt: Text("Enter your collapse id here..."), label: {}) 189 | .textFieldStyle(.roundedBorder) 190 | } 191 | Input(label: "Notification id", help: "A canonical UUID that is the unique ID for the notification. If an error occurs when sending the notification, APNs includes this value when reporting the error to your server. \n\nCanonical UUIDs are 32 lowercase hexadecimal digits, displayed in five groups separated by hyphens in the form 8-4-4-4-12. \n\nAn example looks like this: 123e4567-e89b-12d3- a456-4266554400a0. If vou omit this header, APNs creates a UUID for you and returns it in its response.") { 192 | TextField(text: $appState.notificationId, prompt: Text("Enter your notification id here..."), label: {}) 193 | .textFieldStyle(.roundedBorder) 194 | } 195 | Input(label: "Expiration", help: "The date at which the notification is no longer valid. This value is a UNIX epoch expressed in seconds (UTC). \n\nIf the value is nonzero, APNs stores the notification and tries to deliver it at least once, repeating the attempt as needed until the specified date. \n\nIf the value is 0, APNs attempts to deliver the notification only once and doesn't store it.") { 196 | TextField(text: $appState.expiration, prompt: Text("Enter your expiration here..."), label: {}) 197 | .textFieldStyle(.roundedBorder) 198 | } 199 | } 200 | } 201 | 202 | private var topicForm: some View { 203 | Input(label: "Bundle id", help: "The topic for the notification. \nMost of the time, the topic is your app's bundle ID/app ID. It can have a suffix based on the type of push notification. \nIf you are using a certificate that supports Pushkit VolP or watchOS complication notifications, you must include this header with bundle ID of you app and if applicable, the proper suffix. \nIf you are using token-based authentication with APNs, you must include this header with the correct bundle ID and suffix combination") { 204 | switch appState.selectedCertificateType { 205 | case .keychain: 206 | Picker(selection: $appState.selectedTopic, content: { 207 | ForEach(appState.topics, id: \.self) { 208 | Text($0) 209 | } 210 | }, label: {}) 211 | case .p8: 212 | TextField(text: $appState.selectedTopic, prompt: Text("com.qeude.Swush"), label: {}) 213 | .textFieldStyle(.roundedBorder) 214 | } 215 | } 216 | } 217 | 218 | private var payloadForm: some View { 219 | VStack(alignment: .leading, spacing: 16) { 220 | Text("Payload").font(.title).bold() 221 | TextEditor(text: $appState.payload) 222 | .font(.system(.body, design: .monospaced)) 223 | .fixedSize(horizontal: false, vertical: true) 224 | .cornerRadius(8) 225 | } 226 | } 227 | 228 | private func setup() { 229 | selectedRawCertificateType = appState.selectedCertificateType.rawValue 230 | switch appState.selectedCertificateType { 231 | case .keychain(let certificate): 232 | selectedIdentity = certificate 233 | case .p8(let filepath, let teamId, let keyId): 234 | self.filepath = filepath 235 | self.teamId = teamId 236 | self.keyId = keyId 237 | } 238 | } 239 | } 240 | 241 | struct SenderView_Previews: PreviewProvider { 242 | static var previews: some View { 243 | SenderView() 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/Settings/GeneralSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralSettingsView.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 30/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GeneralSettingsView: View { 11 | @EnvironmentObject var viewModel: UpdaterViewModel 12 | 13 | var body: some View { 14 | Form { 15 | Toggle("Automatically check for updates", isOn: $viewModel.automaticallyChecksForUpdates) 16 | } 17 | .padding(20) 18 | .frame(width: 375, height: 150) 19 | } 20 | } 21 | 22 | struct GeneralSettingsView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | GeneralSettingsView() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Swush/Sources/Modules/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 30/01/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | @EnvironmentObject var viewModel: UpdaterViewModel 12 | 13 | private enum Tabs: Hashable { 14 | case general, advanced 15 | } 16 | 17 | var body: some View { 18 | TabView { 19 | GeneralSettingsView() 20 | .tabItem { 21 | Label("General", systemImage: "gear") 22 | } 23 | .tag(Tabs.general) 24 | } 25 | .padding(20) 26 | } 27 | } 28 | 29 | struct SettingsView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | SettingsView() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Swush/Sources/Services/APNSService+Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APNSService+Error.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 13/02/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension APNSService { 11 | struct APNSError: Decodable { 12 | let reason: String 13 | 14 | var apiError: APIError { 15 | APIError.from(rawValue: reason) 16 | } 17 | } 18 | 19 | enum APIError: Error { 20 | case badDeviceToken 21 | case badPriority 22 | case badTopic 23 | case deviceTokenNotForTopic 24 | case payloadEmpty 25 | case invalidProviderToken 26 | case expiredProviderToken 27 | case unknown 28 | 29 | private static var mapping: [String: APIError] = [ 30 | "BadDeviceToken": .badDeviceToken, 31 | "BadPriority": .badPriority, 32 | "BadTopic": .badTopic, 33 | "DeviceTokenNotForTopic": .deviceTokenNotForTopic, 34 | "PayloadEmpty": .payloadEmpty, 35 | "InvalidProviderToken": .invalidProviderToken, 36 | "ExpiredProviderToken": .expiredProviderToken 37 | ] 38 | 39 | private static var descriptionMapping: [APIError: String] = [ 40 | .badDeviceToken: "The specified device token is invalid. Verify that the request contains a valid token and that the token matches the environment.", 41 | .badPriority: "The provided priority is invalid.", 42 | .badTopic: "The provided topic is invalid.", 43 | .deviceTokenNotForTopic: "The device token doesn’t match the specified topic.", 44 | .payloadEmpty: "Your payload is empty. Please provide a valid payload", 45 | .invalidProviderToken: "The provider token is not valid, or the token signature can't be verified.", 46 | .expiredProviderToken: "The provider token is stale and a new token should be generated.", 47 | .unknown: "An unknown error happened while sending your APNs.", 48 | ] 49 | 50 | var description: String { 51 | return APNSService.APIError.descriptionMapping[self] ?? "An unknown error happened while sending your APNs." 52 | } 53 | 54 | static func from(rawValue: String) -> APIError { 55 | return APNSService.APIError.mapping[rawValue] ?? .unknown 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Swush/Sources/Services/APNSService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APNSService.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 26/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | class APNSService: NSObject { 11 | private var session: URLSession? 12 | private var identity: SecIdentity? 13 | 14 | func sendPush(for apns: APNS) async throws { 15 | if case .keychain(let identity) = apns.certificateType { 16 | self.identity = identity 17 | } 18 | 19 | switch apns.certificateType { 20 | case .keychain: 21 | session = URLSession(configuration: .default, delegate: self, delegateQueue: .main) 22 | case .p8: 23 | session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main) 24 | } 25 | guard let session = session else { return } 26 | 27 | var request = URLRequest( 28 | url: URL( 29 | string: 30 | "https://api.\(apns.isSandbox ? "development." : "")push.apple.com/3/device/\(apns.deviceToken)" 31 | )!) 32 | request.httpMethod = "POST" 33 | request.httpBody = try JSONSerialization.data(withJSONObject: apns.payload!) 34 | 35 | if case .p8 = apns.certificateType { 36 | request.addValue("bearer \(apns.jwt)", forHTTPHeaderField: "authorization") 37 | } 38 | request.addValue(apns.topic, forHTTPHeaderField: "apns-topic") 39 | request.addValue(String(apns.priority.rawValue), forHTTPHeaderField: "apns-priority") 40 | request.addValue(apns.payloadType.rawValue, forHTTPHeaderField: "apns-push-type") 41 | if !apns.collapseId.isEmpty { 42 | request.addValue(apns.collapseId, forHTTPHeaderField: "apns-collapse-id") 43 | } 44 | if !apns.notificationId.isEmpty { 45 | request.addValue(apns.notificationId, forHTTPHeaderField: "apns-notification-id") 46 | } 47 | if !apns.expiration.isEmpty { 48 | request.addValue(apns.expiration, forHTTPHeaderField: "apns-expiration") 49 | } 50 | 51 | let (data, response) = try await session.data(for: request) 52 | guard let status = response.status else { fatalError() } 53 | if !(200...299).contains(status) { 54 | var apnsError: APNSError? = nil 55 | do { 56 | apnsError = try JSONDecoder().decode(APNSError.self, from: data) 57 | } catch { 58 | print("Unable to decode error: \(error)") 59 | } 60 | if let apnsError = apnsError { throw apnsError.apiError } 61 | } 62 | } 63 | } 64 | 65 | extension APNSService: URLSessionDelegate { 66 | func urlSession(_: URLSession, didReceive _: URLAuthenticationChallenge) async 67 | -> (URLSession.AuthChallengeDisposition, URLCredential?) 68 | { 69 | guard let identity = identity else { return (URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)} 70 | var certificate: SecCertificate? 71 | SecIdentityCopyCertificate(identity, &certificate) 72 | let cred = URLCredential( 73 | identity: identity, certificates: [certificate!], persistence: .forSession 74 | ) 75 | return (URLSession.AuthChallengeDisposition.useCredential, cred) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Swush/Sources/Services/DependencyProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependencyManager.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 29/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DependencyProvider { 11 | static var secIdentityService: SecIdentityService { 12 | SecIdentityService() 13 | } 14 | 15 | static var apnsService: APNSService { 16 | APNSService() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Swush/Sources/Services/SecIdentityService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APNSSecIdentityType.swift 3 | // Swush 4 | // 5 | // Created by Quentin Eude on 26/01/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SecIdentityService { 11 | var identities: [SecIdentity]? { 12 | let query: [String: Any] = [ 13 | kSecClass as String: kSecClassIdentity, 14 | kSecMatchLimit as String: kSecMatchLimitAll, 15 | kSecReturnRef as String: kCFBooleanTrue!, 16 | ] 17 | var itemCopy: AnyObject? 18 | let status = SecItemCopyMatching(query as CFDictionary, &itemCopy) 19 | 20 | guard status != errSecItemNotFound else { 21 | // throw KeychainError.itemNotFound 22 | return nil 23 | } 24 | let result = itemCopy as? [SecIdentity] ?? [] 25 | 26 | return result.filter { identity in 27 | identity.type != .invalid 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Swush/Swush.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | # app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app 2 | # apple_id("[[APPLE_ID]]") # Your Apple email address 3 | 4 | 5 | # For more information about the Appfile, see: 6 | # https://docs.fastlane.tools/advanced/#appfile 7 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:mac) 17 | 18 | platform :mac do 19 | 20 | desc "Release the app through Sparkle" 21 | lane :generate_release do 22 | sh("rm -rf ../Build") 23 | clear_derived_data 24 | version = get_version_number 25 | build = get_build_number 26 | build_app( 27 | scheme: "Swush - Release", 28 | clean: true, 29 | skip_codesigning: true, 30 | silent: true, 31 | export_method: "mac-application", 32 | output_directory: "Build/" 33 | ) 34 | zip( 35 | path: "Build/Swush.app", 36 | output_path: "Releases/Swush-#{version}.zip" 37 | ) 38 | sh("./generate_appcast ../Releases/") 39 | github_release = set_github_release( 40 | repository_name: ENV["GITHUB_REPO"], 41 | api_token: ENV["GITHUB_API_TOKEN"], 42 | name: "v#{version} 🚀", 43 | tag_name: "v#{version}", 44 | description: (File.read("../CHANGELOG") rescue "No changelog provided"), 45 | commitish: "main", 46 | upload_assets: ["Releases/Swush-#{version}.zip"] 47 | ) 48 | end 49 | 50 | 51 | desc "Bump version and build number of the app" 52 | lane :bump_version do |options| 53 | mode = "minor" 54 | if options[:mode] 55 | if ["patch", "minor", "major"].include? options[:mode] 56 | mode = options[:mode] 57 | else 58 | UI.user_error!("You should set a mode in the following: \"patch\", \"minor\", \"major\"") 59 | end 60 | end 61 | sh("git checkout develop") 62 | version = increment_version_number( 63 | bump_type: mode 64 | ) 65 | build = increment_build_number 66 | sh("git commit -am '🔖 bump to version v#{version}'") 67 | sh("git push") 68 | sh("git checkout main && git merge develop && git push") 69 | add_git_tag(tag: "v#{version}") 70 | push_git_tags 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## Mac 17 | 18 | ### mac generate_release 19 | 20 | ```sh 21 | [bundle exec] fastlane mac generate_release 22 | ``` 23 | 24 | Release the app through Sparkle 25 | 26 | ### mac bump_version 27 | 28 | ```sh 29 | [bundle exec] fastlane mac bump_version 30 | ``` 31 | 32 | Bump version and build number of the app 33 | 34 | ---- 35 | 36 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 37 | 38 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 39 | 40 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 41 | -------------------------------------------------------------------------------- /fastlane/generate_appcast: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/fastlane/generate_appcast -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/icon.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qeude/Swush/e69846f2791e14656adeda1002f6c07b339f01eb/screenshot.png --------------------------------------------------------------------------------