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