";
1263 | };
1264 | "OBJ_90" = {
1265 | isa = "PBXBuildFile";
1266 | fileRef = "OBJ_42";
1267 | };
1268 | "OBJ_91" = {
1269 | isa = "PBXBuildFile";
1270 | fileRef = "OBJ_43";
1271 | };
1272 | "OBJ_92" = {
1273 | isa = "PBXBuildFile";
1274 | fileRef = "OBJ_44";
1275 | };
1276 | "OBJ_93" = {
1277 | isa = "PBXBuildFile";
1278 | fileRef = "OBJ_45";
1279 | };
1280 | "OBJ_94" = {
1281 | isa = "PBXBuildFile";
1282 | fileRef = "OBJ_46";
1283 | };
1284 | "OBJ_95" = {
1285 | isa = "PBXBuildFile";
1286 | fileRef = "OBJ_47";
1287 | };
1288 | "OBJ_96" = {
1289 | isa = "PBXBuildFile";
1290 | fileRef = "OBJ_48";
1291 | };
1292 | "OBJ_97" = {
1293 | isa = "PBXBuildFile";
1294 | fileRef = "OBJ_50";
1295 | };
1296 | "OBJ_98" = {
1297 | isa = "PBXBuildFile";
1298 | fileRef = "OBJ_51";
1299 | };
1300 | "OBJ_99" = {
1301 | isa = "PBXBuildFile";
1302 | fileRef = "OBJ_52";
1303 | };
1304 | "Poes::Poes" = {
1305 | isa = "PBXNativeTarget";
1306 | buildConfigurationList = "OBJ_105";
1307 | buildPhases = (
1308 | "OBJ_108",
1309 | "OBJ_110"
1310 | );
1311 | dependencies = (
1312 | "OBJ_113",
1313 | "OBJ_115"
1314 | );
1315 | name = "Poes";
1316 | productName = "Poes";
1317 | productReference = "Poes::Poes::Product";
1318 | productType = "com.apple.product-type.tool";
1319 | };
1320 | "Poes::Poes::Product" = {
1321 | isa = "PBXFileReference";
1322 | path = "Poes";
1323 | sourceTree = "BUILT_PRODUCTS_DIR";
1324 | };
1325 | "Poes::PoesCore" = {
1326 | isa = "PBXNativeTarget";
1327 | buildConfigurationList = "OBJ_116";
1328 | buildPhases = (
1329 | "OBJ_119",
1330 | "OBJ_125"
1331 | );
1332 | dependencies = (
1333 | "OBJ_127"
1334 | );
1335 | name = "PoesCore";
1336 | productName = "PoesCore";
1337 | productReference = "Poes::PoesCore::Product";
1338 | productType = "com.apple.product-type.framework";
1339 | };
1340 | "Poes::PoesCore::Product" = {
1341 | isa = "PBXFileReference";
1342 | path = "PoesCore.framework";
1343 | sourceTree = "BUILT_PRODUCTS_DIR";
1344 | };
1345 | "Poes::PoesPackageTests::ProductTarget" = {
1346 | isa = "PBXAggregateTarget";
1347 | buildConfigurationList = "OBJ_135";
1348 | buildPhases = (
1349 | );
1350 | dependencies = (
1351 | "OBJ_138"
1352 | );
1353 | name = "PoesPackageTests";
1354 | productName = "PoesPackageTests";
1355 | };
1356 | "Poes::PoesTests" = {
1357 | isa = "PBXNativeTarget";
1358 | buildConfigurationList = "OBJ_140";
1359 | buildPhases = (
1360 | "OBJ_143",
1361 | "OBJ_146"
1362 | );
1363 | dependencies = (
1364 | "OBJ_149",
1365 | "OBJ_150"
1366 | );
1367 | name = "PoesTests";
1368 | productName = "PoesTests";
1369 | productReference = "Poes::PoesTests::Product";
1370 | productType = "com.apple.product-type.bundle.unit-test";
1371 | };
1372 | "Poes::PoesTests::Product" = {
1373 | isa = "PBXFileReference";
1374 | path = "PoesTests.xctest";
1375 | sourceTree = "BUILT_PRODUCTS_DIR";
1376 | };
1377 | "Poes::SwiftPMPackageDescription" = {
1378 | isa = "PBXNativeTarget";
1379 | buildConfigurationList = "OBJ_129";
1380 | buildPhases = (
1381 | "OBJ_132"
1382 | );
1383 | dependencies = (
1384 | );
1385 | name = "PoesPackageDescription";
1386 | productName = "PoesPackageDescription";
1387 | productType = "com.apple.product-type.framework";
1388 | };
1389 | "swift-argument-parser::ArgumentParser" = {
1390 | isa = "PBXNativeTarget";
1391 | buildConfigurationList = "OBJ_71";
1392 | buildPhases = (
1393 | "OBJ_74",
1394 | "OBJ_103"
1395 | );
1396 | dependencies = (
1397 | );
1398 | name = "ArgumentParser";
1399 | productName = "ArgumentParser";
1400 | productReference = "swift-argument-parser::ArgumentParser::Product";
1401 | productType = "com.apple.product-type.framework";
1402 | };
1403 | "swift-argument-parser::ArgumentParser::Product" = {
1404 | isa = "PBXFileReference";
1405 | path = "ArgumentParser.framework";
1406 | sourceTree = "BUILT_PRODUCTS_DIR";
1407 | };
1408 | "swift-argument-parser::SwiftPMPackageDescription" = {
1409 | isa = "PBXNativeTarget";
1410 | buildConfigurationList = "OBJ_152";
1411 | buildPhases = (
1412 | "OBJ_155"
1413 | );
1414 | dependencies = (
1415 | );
1416 | name = "swift-argument-parserPackageDescription";
1417 | productName = "swift-argument-parserPackageDescription";
1418 | productType = "com.apple.product-type.framework";
1419 | };
1420 | };
1421 | rootObject = "OBJ_1";
1422 | }
1423 |
--------------------------------------------------------------------------------
/Poes.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Poes.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Poes.xcodeproj/xcshareddata/xcschemes/Poes-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
48 |
54 |
55 |
56 |
57 |
58 |
68 |
69 |
75 |
76 |
78 |
79 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Poes.xcodeproj/xcshareddata/xcschemes/Poes.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
71 |
72 |
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Poes
2 | A Swift command-line tool to easily send push notifications to the iOS simulator.
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Poes helps you with:
15 |
16 | - [x] Generating a JSON payload for push notifications
17 | - [x] Sending and testing push notifications in the simulator
18 |
19 | ### Requirements
20 | - Xcode 11.4 beta 1 and up
21 |
22 | ### Usage
23 |
24 | ```
25 | $ poes --help
26 | OVERVIEW: A Swift command-line tool to easily test push notifications to the
27 | iOS simulator
28 |
29 | USAGE: poes
30 |
31 | OPTIONS:
32 | -h, --help Show help information.
33 |
34 | SUBCOMMANDS:
35 | send Send a push notification to an app installed on the
36 | iOS Simulator
37 | ```
38 |
39 |
40 |
41 | ```
42 | $ poes send --help
43 | OVERVIEW: Send a push notification to an app installed on the iOS Simulator
44 |
45 | USAGE: poes send [--title ] [--body ] [--badge ] [--is-mutable] [--verbose]
46 |
47 | ARGUMENTS:
48 | The bundle identifier of the app to push to
49 |
50 | OPTIONS:
51 | -t, --title The title of the Push notification (default: Default
52 | Title)
53 | -b, --body The body of the Push notification (default: Default
54 | Body)
55 | -b, --badge The number to display in a badge on your app’s icon
56 | -i, --is-mutable Adds the mutable-content key to the payload
57 | --verbose Show extra logging for debugging purposes
58 | -h, --help Show help information.
59 | ```
60 |
61 | The bundle identifier is mandatory, all others have a default value. The following command can be enough to send out a notification:
62 |
63 | ```
64 | $ poes send com.wetransfer.app --verbose
65 | Generated payload:
66 |
67 | {
68 | "aps" : {
69 | "alert" : {
70 | "title" : "Default title",
71 | "body" : "Default body"
72 | },
73 | "mutable-content" : false
74 | }
75 | }
76 |
77 | Sending push notification...
78 | Push notification sent successfully
79 | ```
80 |
81 | ### Installation using [Mint](https://github.com/yonaskolb/mint)
82 | You can install Poes using Mint as follows:
83 |
84 | ```
85 | $ mint install AvdLee/Poes
86 | ```
87 |
88 | ### Development
89 | - `cd` into the repository
90 | - run `swift package generate-xcodeproj` (Generates an Xcode project for development)
91 | - Run the following command to try it out:
92 |
93 | ```bash
94 | swift run Poes --help
95 | ```
96 |
97 | ## FAQ
98 |
99 | ### Why is it called "Poes"?
100 |
101 | Poes is a Dutch word for a female cat. The pronunciation is the same as "Push" and pushing notifications is what we're doing here!
102 |
103 | ### Why is there a `PoesCore` framework?
104 | This makes it really easy to eventually create a Mac App with a UI around it 🚀
105 |
106 | ### How do I create a Swift Package myself?
107 | Check out my blog post [Swift Package framework creation in Xcode](https://www.avanderlee.com/swift/creating-swift-package-manager-framework/).
108 |
109 | ### Can I learn more about testing Push Notifications on the iOS simulator?
110 | Yes! I've written a detailed blog post about this: [Testing push notifications on the iOS simulator](https://www.avanderlee.com/workflow/testing-push-notifications-ios-simulator/)
111 |
--------------------------------------------------------------------------------
/Sources/Poes/main.swift:
--------------------------------------------------------------------------------
1 | import PoesCore
2 |
3 | Poes.main()
4 |
--------------------------------------------------------------------------------
/Sources/PoesCore/Helpers/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 AvdLee. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum Log {
12 | static var isVerbose: Bool = false
13 |
14 | static func debug(_ message: Any) {
15 | guard isVerbose else { return }
16 | print(message)
17 | }
18 |
19 | static func message(_ message: Any) {
20 | print(message)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PoesCore/Helpers/Shell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Shell.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 AvdLee. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum ShellCommand {
12 | case push(bundleIdentifier: String, payloadPath: String)
13 |
14 | var rawValue: String {
15 | switch self {
16 | case .push(let bundleIdentifier, let payloadPath):
17 | return "xcrun simctl push booted \(bundleIdentifier) \(payloadPath)"
18 | }
19 | }
20 | }
21 |
22 | extension Process {
23 | func shell(_ command: ShellCommand) -> String {
24 | launchPath = "/bin/bash"
25 | arguments = ["-c", command.rawValue]
26 |
27 | let outputPipe = Pipe()
28 | standardOutput = outputPipe
29 | launch()
30 |
31 | let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
32 | guard let outputData = String(data: data, encoding: String.Encoding.utf8) else { return "" }
33 |
34 | return outputData.reduce("") { (result, value) in
35 | return result + String(value)
36 | }.trimmingCharacters(in: .whitespacesAndNewlines)
37 | }
38 | }
39 |
40 | protocol ShellExecuting {
41 | @discardableResult static func execute(_ command: ShellCommand) -> String
42 | }
43 |
44 | private enum Shell: ShellExecuting {
45 | @discardableResult static func execute(_ command: ShellCommand) -> String {
46 | return Process().shell(command)
47 | }
48 | }
49 |
50 | /// Adds a `shell` property which defaults to `Shell.self`.
51 | protocol ShellInjectable { }
52 |
53 | extension ShellInjectable {
54 | static var shell: ShellExecuting.Type { ShellInjector.shell }
55 | }
56 |
57 | enum ShellInjector {
58 | static var shell: ShellExecuting.Type = Shell.self
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/PoesCore/Payload.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Payload.swift
3 | // PoesCore
4 | //
5 | // Created by Antoine van der Lee on 07/02/2020.
6 | // Copyright © 2020 AvdLee. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Payload: Codable {
12 | let aps: APS
13 |
14 | init(title: String, body: String, isMutable: Bool, badge: Int? = nil) {
15 | aps = APS(alert: Alert(title: title, body: body), isMutable: isMutable, badge: badge)
16 | }
17 | }
18 |
19 | struct APS: Codable {
20 | enum CodingKeys: String, CodingKey {
21 | case alert
22 | case isMutable = "mutable-content"
23 | case badge = "badge"
24 | }
25 |
26 | let alert: Alert
27 | let isMutable: Bool
28 | let badge: Int?
29 | }
30 |
31 | struct Alert: Codable {
32 | let title: String
33 | let body: String
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/PoesCore/Poes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Poes.swift
3 | // PoesCore
4 | //
5 | // Created by Antoine van der Lee on 07/02/2020.
6 | // Copyright © 2020 AvdLee. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ArgumentParser
11 |
12 | public struct Poes: ParsableCommand {
13 | public static let configuration = CommandConfiguration(
14 | abstract: "A Swift command-line tool to easily test push notifications to the iOS simulator",
15 | subcommands: [Send.self])
16 |
17 | public init() { }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/PoesCore/Send.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Send.swift
3 | // PoesCore
4 | //
5 | // Created by Antoine van der Lee on 08/02/2020.
6 | // Copyright © 2020 AvdLee. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ArgumentParser
11 |
12 | struct Send: ParsableCommand, ShellInjectable {
13 |
14 | public static let configuration = CommandConfiguration(abstract: "Send a push notification to an app installed on the iOS Simulator")
15 |
16 | @Argument(help: "The bundle identifier of the app to push to")
17 | private var bundleIdentifier: String
18 |
19 | @Option(name: .shortAndLong, default: "Default Title", help: "The title of the Push notification")
20 | private var title: String
21 |
22 | @Option(name: .shortAndLong, default: "Default Body", help: "The body of the Push notification")
23 | private var body: String
24 |
25 | @Option(name: .shortAndLong, help: "The number to display in a badge on your app’s icon")
26 | private var badge: Int?
27 |
28 | @Flag(name: .shortAndLong, help: "Adds the mutable-content key to the payload")
29 | private var isMutable: Bool
30 |
31 | @Flag(name: .long, help: "Show extra logging for debugging purposes")
32 | private var verbose: Bool
33 |
34 | func run() throws {
35 | Log.isVerbose = verbose
36 |
37 | let payload = Payload(title: title, body: body, isMutable: isMutable, badge: badge)
38 | let jsonData = try JSONEncoder().encode(payload)
39 |
40 | if Log.isVerbose, let object = try? JSONSerialization.jsonObject(with: jsonData, options: []), let jsonString = String(data: try! JSONSerialization.data(withJSONObject: object, options: .prettyPrinted), encoding: .utf8) {
41 | Log.debug("Generated payload:\n\n\(jsonString)\n")
42 | }
43 |
44 | let tempUrl = NSTemporaryDirectory()
45 | let payloadUrl = Foundation.URL(fileURLWithPath: tempUrl, isDirectory: true).appendingPathComponent("payload.json")
46 | FileManager.default.createFile(atPath: payloadUrl.path, contents: jsonData, attributes: nil)
47 |
48 | Log.message("Sending push notification...")
49 | Self.shell.execute(.push(bundleIdentifier: bundleIdentifier, payloadPath: payloadUrl.path))
50 | Log.message("Push notification sent successfully")
51 | try FileManager.default.removeItem(at: payloadUrl)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | fatalError("Running tests like this is unsupported. Run the tests again by using `swift test --enable-test-discovery`")
2 |
--------------------------------------------------------------------------------
/Tests/PoesTests/Mocks.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Mocks.swift
3 | // PoesTests
4 | //
5 | // Created by Antoine van der Lee on 08/02/2020.
6 | // Copyright © 2020 AvdLee. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @testable import PoesCore
11 |
12 | struct MockedShell: ShellExecuting {
13 |
14 | static var commandMocks: [String: String] = [:]
15 | static private(set) var executedCommand: ShellCommand?
16 |
17 | @discardableResult static func execute(_ command: ShellCommand) -> String {
18 | executedCommand = command
19 | return commandMocks[command.rawValue] ?? ""
20 | }
21 |
22 | static func mock(_ command: ShellCommand, value: String) {
23 | commandMocks[command.rawValue] = value
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/PoesTests/PoesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PoesTests.swift
3 | // PoesTests
4 | //
5 | // Created by Antoine van der Lee on 07/02/2020.
6 | // Copyright © 2020 AvdLee. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import PoesCore
11 |
12 | final class PoesTests: XCTestCase {
13 |
14 | override class func setUp() {
15 | super.setUp()
16 |
17 | ShellInjector.shell = MockedShell.self
18 | }
19 |
20 | /// It should correctly generate a JSON payload for sending push notifications.
21 | func testSendCommand() throws {
22 | let expectedBundleIdentifier = "com.example.app"
23 | let title = "Notification title"
24 | let body = "Notification body"
25 | let badge = 2
26 | let isMutable = true
27 |
28 | let arguments = ["send", expectedBundleIdentifier, "--title", title, "--body", body, "--badge", "\(badge)", "--is-mutable"]
29 |
30 | let poesCommand = try Poes.parseAsRoot(arguments)
31 | try poesCommand.run()
32 |
33 | let command = try XCTUnwrap(MockedShell.executedCommand)
34 |
35 | guard case let ShellCommand.push(bundleIdentifier, payloadPath) = command else {
36 | XCTFail("Command is not executed")
37 | return
38 | }
39 |
40 | XCTAssertEqual(expectedBundleIdentifier, bundleIdentifier)
41 |
42 | let jsonData = try Data(contentsOf: URL(fileURLWithPath: payloadPath))
43 | let payload = try JSONDecoder().decode(Payload.self, from: jsonData)
44 |
45 | XCTAssertEqual(payload.aps.alert.title, title)
46 | XCTAssertEqual(payload.aps.alert.body, body)
47 | XCTAssertEqual(payload.aps.isMutable, isMutable)
48 | XCTAssertEqual(payload.aps.badge, badge)
49 |
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/fastlane/.gitignore:
--------------------------------------------------------------------------------
1 | installer/
2 | test_output/
3 | README.md
4 | report.xml
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # Fastlane requirements
2 | fastlane_version "1.109.0"
3 |
4 | import "./../Submodules/WeTransfer-iOS-CI/Fastlane/Fastfile"
5 |
6 | desc "Run the tests and prepare for Danger"
7 | lane :test do |options|
8 | spm(command: "generate-xcodeproj")
9 | test_project(
10 | project_name: "Poes",
11 | scheme: "Poes-Package",
12 | device: nil,
13 | destination: "platform=macOS")
14 | end
15 |
--------------------------------------------------------------------------------