diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..2f6e4e0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @fpseverino \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a8cca0d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,11 @@ +name: test +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +on: + pull_request: { types: [opened, reopened, synchronize, ready_for_review] } + push: { branches: [ main ] } + +jobs: + unit-tests: + uses: vapor/ci/.github/workflows/run-unit-tests.yml@main \ No newline at end of file diff --git a/.gitignore b/.gitignore index 95c4320..17f73d0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /Packages /*.xcodeproj xcuserdata/ +Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index f73f4de..0000000 --- a/Package.resolved +++ /dev/null @@ -1,187 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "apns", - "repositoryURL": "https://github.com/vapor/apns.git", - "state": { - "branch": null, - "revision": "82912b280a6c7be894b2969ab09db36e8be8cde6", - "version": "1.0.0-beta.2.4" - } - }, - { - "package": "apnswift", - "repositoryURL": "https://github.com/kylebrowning/APNSwift.git", - "state": { - "branch": null, - "revision": "b5ca1c9279a1748c84c05e83581cc2d739f28380", - "version": "1.7.0" - } - }, - { - "package": "async-http-client", - "repositoryURL": "https://github.com/swift-server/async-http-client.git", - "state": { - "branch": null, - "revision": "48e284d1ea6d0e8baac1af1c4ad8bd298670caf6", - "version": "1.0.1" - } - }, - { - "package": "async-kit", - "repositoryURL": "https://github.com/vapor/async-kit.git", - "state": { - "branch": null, - "revision": "cb8e6ee62dc6684a95132f8d46511956e78ee0f1", - "version": "1.0.0-beta.2" - } - }, - { - "package": "console-kit", - "repositoryURL": "https://github.com/vapor/console-kit.git", - "state": { - "branch": null, - "revision": "535874e4654b17cebb422b9e17353e85d421a118", - "version": "4.0.0-beta.2" - } - }, - { - "package": "fluent", - "repositoryURL": "https://github.com/vapor/fluent.git", - "state": { - "branch": null, - "revision": "fa4382fd2eccd83e86a5a9c8aa18ff037dee13a2", - "version": "4.0.0-beta.2.2" - } - }, - { - "package": "fluent-kit", - "repositoryURL": "https://github.com/vapor/fluent-kit.git", - "state": { - "branch": null, - "revision": "7dc374e6bf26f0bb2bbc55426d1b94668b542bef", - "version": "1.0.0-beta.2.9" - } - }, - { - "package": "multipart-kit", - "repositoryURL": "https://github.com/vapor/multipart-kit.git", - "state": { - "branch": null, - "revision": "b41a49b5756ac3fbf8f2b07228a7e75f9b60731a", - "version": "4.0.0-beta.2" - } - }, - { - "package": "open-crypto", - "repositoryURL": "https://github.com/vapor/open-crypto.git", - "state": { - "branch": null, - "revision": "90c49bc68ee6d992fa13cf84ca8fc54b97eaf4cc", - "version": "4.0.0-beta.2" - } - }, - { - "package": "routing-kit", - "repositoryURL": "https://github.com/vapor/routing-kit.git", - "state": { - "branch": null, - "revision": "6a8a1636ad26494b03f3c72d74a420fc3a44949c", - "version": "4.0.0-beta.3" - } - }, - { - "package": "sql-kit", - "repositoryURL": "https://github.com/vapor/sql-kit.git", - "state": { - "branch": null, - "revision": "d09b5527fb0c341f6993e4f5233fadbb7dee1c76", - "version": "3.0.0-beta.3" - } - }, - { - "package": "swift-backtrace", - "repositoryURL": "https://github.com/ianpartridge/swift-backtrace.git", - "state": { - "branch": null, - "revision": "eaf2cef011c0c23d1701aa60b364def8015dc3c7", - "version": "1.1.1" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", - "version": "1.2.0" - } - }, - { - "package": "swift-metrics", - "repositoryURL": "https://github.com/apple/swift-metrics.git", - "state": { - "branch": null, - "revision": "3fefedaaef285830cc98ae80231140122076a7e0", - "version": "1.2.0" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "f6487a11d80bfb9a0a0a752b7442847c7e3a8253", - "version": "2.12.0" - } - }, - { - "package": "swift-nio-extras", - "repositoryURL": "https://github.com/apple/swift-nio-extras.git", - "state": { - "branch": null, - "revision": "53808818c2015c45247cad74dc05c7a032c96a2f", - "version": "1.3.2" - } - }, - { - "package": "swift-nio-http2", - "repositoryURL": "https://github.com/apple/swift-nio-http2.git", - "state": { - "branch": null, - "revision": "c1bfb7ce3f201e41ff60ef38fa63e67e0eb66a24", - "version": "1.9.0" - } - }, - { - "package": "swift-nio-ssl", - "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", - "state": { - "branch": null, - "revision": "b75ffaba05b2cffdb1420d558f1a90b4e6c46dcc", - "version": "2.5.0" - } - }, - { - "package": "vapor", - "repositoryURL": "https://github.com/vapor/vapor.git", - "state": { - "branch": null, - "revision": "b3e87b876d88d401583bba0b41db25b02e405e2a", - "version": "4.0.0-beta.3.3" - } - }, - { - "package": "websocket-kit", - "repositoryURL": "https://github.com/vapor/websocket-kit.git", - "state": { - "branch": null, - "revision": "4c9da4fe7d8186243418254031288ab5ee0202e4", - "version": "2.0.0-beta.2.1" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 502db4a..3f664f6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,32 +1,51 @@ -// swift-tools-version:5.1 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:5.9 import PackageDescription let package = Package( name: "PassKit", platforms: [ - .macOS(.v10_15), .iOS(.v13) + .macOS(.v13), .iOS(.v16) ], products: [ - .library( - name: "PassKit", - targets: ["PassKit"]), + .library(name: "PassKit", targets: ["PassKit"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta"), - .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-beta"), - .package(url: "https://github.com/vapor/apns.git", from: "1.0.0-beta"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0") + .package(url: "https://github.com/vapor/vapor.git", from: "4.92.4"), + .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), + .package(url: "https://github.com/vapor/apns.git", from: "4.0.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4") ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "PassKit", - dependencies: ["Fluent", "Vapor", "APNS", "Logging"]), + dependencies: [ + .product(name: "Fluent", package: "fluent"), + .product(name: "Vapor", package: "vapor"), + .product(name: "VaporAPNS", package: "apns"), + .product(name: "Logging", package: "swift-log") + ], + swiftSettings: swiftSettings + ), .testTarget( name: "PassKitTests", - dependencies: ["PassKit"]), + dependencies: [ + .target(name: "PassKit"), + .product(name: "Fluent", package: "fluent"), + .product(name: "Vapor", package: "vapor"), + .product(name: "VaporAPNS", package: "apns"), + .product(name: "Logging", package: "swift-log") + ], + swiftSettings: swiftSettings + ), ] ) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), +// .enableUpcomingFeature("StrictConcurrency"), +// .enableExperimentalFeature("StrictConcurrency=complete"), +] } diff --git a/README.md b/README.md index 7c0e6ef..db51105 100644 --- a/README.md +++ b/README.md @@ -3,111 +3,124 @@ [![Swift Package Manager compatible](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) [![Platform](https://img.shields.io/badge/Platforms-macOS%20|%20Linux-lightgrey.svg)](https://github.com/gargoylesoft/PassKit) -A Vapor package which handles all the server side elements required to implement passes for iOS. +A Vapor package which handles all the server side elements required to implement passes for Apple Wallet. ## NOTE -This package requires Vapor 4. - +This package requires Vapor 4. ## Usage -### Model the `pass.json` contents - -Create a `struct` that implements `Encodable` which will contain all the fields for the generated `pass.json` file. For information on the various keys -available see [Understanding the Keys](https://developer.apple.com/library/archive/documentation/UserExperience/Reference/PassKit_Bundle/Chapters/Introduction.html). - -```swift -struct PassJsonData: Encodable { - public static let token = "EB80D9C6-AD37-41A0-875E-3802E88CA478" - - private let formatVersion = 1 - private let passTypeIdentifier = "pass.com.yoursite.passType" - private let authenticationToken = token - ... -} -``` - ### Implement your pass data model Your data model should contain all the fields that you store for your pass, as well as a foreign key for the pass itself. ```swift -public class PassData: PassKitPassData { - public static var schema = "pass_data" +final class PassData: PassKitPassData { + static let schema = "pass_data" - @ID(key: "id") - public var id: Int? + @ID + var id: UUID? @Parent(key: "pass_id") - public var pass: PKPass + var pass: PKPass + // Add any other field relative to your app, such as a location, a date, etc. @Field(key: "punches") - public var punches: Int + var punches: Int - public required init() {} + init() {} } -extension PassData: Migration { - public func prepare(on database: Database) -> EventLoopFuture { - database.schema(Self.schema) - .field("id", .int, .identifier(auto: true)) +struct CreatePassData: AsyncMigration { + public func prepare(on database: Database) async throws { + try await database.schema(Self.schema) + .id() .field("punches", .int, .required) - .field("pass_id", .uuid, .required) - .foreignKey("pass_id", references: PKPass.schema, "id", onDelete: .cascade) + .field("pass_id", .uuid, .required, .references(PKPass.schema, .id, onDelete: .cascade)) .create() - .flatMap { - guard let db = database as? PostgresDatabase else { - fatalError("Looks like you're not using PostgreSQL any longer!") - } - - return .andAllSucceed( - trigger.map { db.sql().raw($0).run() }, - on: db.eventLoop - ) - } } public func revert(on database: Database) -> EventLoopFuture { - database.schema(Self.schema).delete() + try await database.schema(Self.schema).delete() } } +``` + +### Handle cleanup -// db.sql().raw() doesn't allow for multiple statements, so make it an array -private let trigger: [SQLQueryString] = [ - """ - CREATE OR REPLACE FUNCTION "public"."UpdateModified"() RETURNS trigger +Depending on your implementation details, you'll likely want to automatically clean out the passes and devices table when a registration is deleted. +You'll need to implement based on your type of SQL database as there's not yet a Fluent way to implement something like SQL's `NOT EXISTS` call with a `DELETE` statement. +If you're using PostgreSQL, you can setup these triggers/methods: + +```sql +CREATE OR REPLACE FUNCTION public."RemoveUnregisteredItems"() RETURNS trigger LANGUAGE plpgsql - AS $$ - BEGIN - UPDATE \(PKPass.schema) - SET modified = now() - WHERE "id" = NEW.pass_id; - - RETURN NEW; - END; - $$; - """, - - """ - DROP TRIGGER IF EXISTS "OnPassDataUpdated" ON "public"."\(PassData.schema)"; - """, - - """ - CREATE TRIGGER "OnPassDataUpdated" - AFTER UPDATE OF "punches" ON "public"."\(PassData.schema)" - FOR EACH ROW - EXECUTE PROCEDURE "public"."UpdateModified"(); - """ -] + AS $$BEGIN + DELETE FROM devices d + WHERE NOT EXISTS ( + SELECT 1 + FROM registrations r + WHERE d."id" = r.device_id + LIMIT 1 + ); + + DELETE FROM passes p + WHERE NOT EXISTS ( + SELECT 1 + FROM registrations r + WHERE p."id" = r.pass_id + LIMIT 1 + ); + + RETURN OLD; +END +$$; + +CREATE TRIGGER "OnRegistrationDelete" +AFTER DELETE ON "public"."registrations" +FOR EACH ROW +EXECUTE PROCEDURE "public"."RemoveUnregisteredItems"(); ``` -**IMPORTANT**: Whenever your pass data changes, you must update the *modified* time of the linked pass so that Apple knows to send you a new pass. The given example, above, is for PostgreSQL, but the concept should be the same for any database. The syntax for the triggers will simply be a little different. You can do this in `ModelMiddleware` but I like to have the database itself do it so if anything outside the app makes a change, it still updates. +### Model the `pass.json` contents + +Create a `struct` that implements `Encodable` which will contain all the fields for the generated `pass.json` file. +Create an initializer that takes your custom pass data, the `PKPass` and everything else you may need. +For information on the various keys available see the [documentation](https://developer.apple.com/documentation/walletpasses/pass). +See also [this guide](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/index.html#//apple_ref/doc/uid/TP40012195-CH1-SW1) for some help. + +```swift +struct PassJsonData: Encodable { + public static let token = "EB80D9C6-AD37-41A0-875E-3802E88CA478" + + private let formatVersion = 1 + private let passTypeIdentifier = "pass.com.yoursite.passType" + private let authenticationToken = token + let serialNumber: String + let relevantDate: String + let barcodes: [PassJsonData.Barcode] + ... + + struct Barcode: Encodable { + let altText: String + let format = "PKBarcodeFormatQR" + let message: String + let messageEncoding = "iso-8859-1" + } + + init(data: PassData, pass: PKPass) { + ... + } +} +``` ### Implement the delegate. -Create a delegate file that implements `PassKitDelegate`. There are other fields available which have reasonable default values. See the delegate's documentation. -Because the files for your pass' template and the method of encoding might vary by pass type, you'll be provided the pass for those methods. +Create a delegate file that implements `PassKitDelegate`. +In the `sslSigningFilesDirectory` you specify there must be the `WWDR.pem`, `passcertificate.pem` and `passkey.pem` files. If they are named like that you're good to go, otherwise you have to specify the custom name. +There are other fields available which have reasonable default values. See the delegate's documentation. +Because the files for your pass' template and the method of encoding might vary by pass type, you'll be provided the pass for those methods. ```swift import Vapor @@ -116,7 +129,7 @@ import PassKit class PKD: PassKitDelegate { var sslSigningFilesDirectory = URL(fileURLWithPath: "/www/myapp/sign", isDirectory: true) - var pemPrivateKeyPassword: String? = "12345" + var pemPrivateKeyPassword: String? = Environment.get("PEM_PRIVATE_KEY_PASSWORD")! func encode(pass: P, db: Database, encoder: JSONEncoder) -> EventLoopFuture { // The specific PassData class you use here may vary based on the pass.type if you have multiple @@ -143,48 +156,11 @@ class PKD: PassKitDelegate { You **must** explicitly declare `pemPrivateKeyPassword` as a `String?` or Swift will ignore it as it'll think it's a `String` instead. -#### Handle cleanup - -Depending on your implementation details, you'll likely want to automatically clean out the passes and devices table when -a registration is deleted. You'll need to implement based on your type of SQL database as there's not yet a Fluent way -to implement something like SQL's `NOT EXISTS` call with a `DELETE` statement. If you're using PostgreSQL, you can -setup these triggers/methods: - -```sql -CREATE OR REPLACE FUNCTION public."RemoveUnregisteredItems"() RETURNS trigger - LANGUAGE plpgsql - AS $$BEGIN - DELETE FROM devices d - WHERE NOT EXISTS ( - SELECT 1 - FROM registrations r - WHERE d."id" = r.device_id - LIMIT 1 - ); - - DELETE FROM passes p - WHERE NOT EXISTS ( - SELECT 1 - FROM registrations r - WHERE p."id" = r.pass_id - LIMIT 1 - ); - - RETURN OLD; -END -$$; - -CREATE TRIGGER "OnRegistrationDelete" -AFTER DELETE ON "public"."registrations" -FOR EACH ROW -EXECUTE PROCEDURE "public"."RemoveUnregisteredItems"(); -``` - ### Register Routes Next, register the routes in `routes.swift`. Notice how the `delegate` is created as -a global variable. You need to ensure that the delegate doesn't go out of scope as soon as the `routes(_:)` method exits! This will -implement all of the routes that PassKit expects to exist on your server for you. +a global variable. You need to ensure that the delegate doesn't go out of scope as soon as the `routes(_:)` method exits! +This will implement all of the routes that PassKit expects to exist on your server for you. ```swift let delegate = PKD() @@ -197,13 +173,10 @@ func routes(_ app: Application) throws { #### Push Notifications -If you wish to include routes specifically for sending push notifications to updated passes you can also include this line in your `routes(_:)` method. You'll -need to pass in whatever `Middleware` you want Vapor to use to authenticate the two routes. Note that PassKit will *not* send a push notification if you -use the sandbox, which is why this method doesn't let you pass the APNs environment type. If you've not yet configured APNSwift, calling this method will -do so for you. +If you wish to include routes specifically for sending push notifications to updated passes you can also include this line in your `routes(_:)` method. You'll need to pass in whatever `Middleware` you want Vapor to use to authenticate the two routes. If you don't include this line, you have to configure an APNS container yourself ```swift -try pk.registerPushRoutes(middleware: PushAuthMiddleware()) +try pk.registerPushRoutes(middleware: SecretMiddleware()) ``` That will add two routes: @@ -211,21 +184,22 @@ That will add two routes: - POST .../api/v1/push/*passTypeIdentifier*/*passBarcode* (Sends notifications) - GET .../api/v1/push/*passTypeIdentifier*/*passBarcode* (Retrieves a list of push tokens which would be sent a notification) -Whether you include the routes or not, you'll want to add a method that sends push notifications when your pass data updates. If you did *not* include -the routes remember to configure APNSwift yourself. You can implement it like so: +Whether you include the routes or not, you'll want to add a middleware that sends push notifications and updates the `modified` field when your pass data updates. You can implement it like so: ```swift -struct PassDataMiddleware: ModelMiddleware { +struct PassDataMiddleware: AsyncModelMiddleware { private unowned let app: Application init(app: Application) { self.app = app } - func update(model: PassData, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - next.update(model, on: db).flatMap { - PassKit.sendPushNotifications(for: model.$pass, on: db, app: self.app) - } + func update(model: PassData, on db: Database, next: AnyAsyncModelResponder) async throws { + let pkPass = try await model.$pass.get(on: db) + pkPass.modified = Date() + try await pkPass.update(on: db) + try await next.update(model, on: db) + try await PassKit.sendPushNotifications(for: model.$pass.get(on: db), on: db, app: self.app) } } ``` @@ -236,6 +210,42 @@ and register it in *configure.swift*: app.databases.middleware.use(PassDataMiddleware(app: app), on: .psql) ``` +**IMPORTANT**: Whenever your pass data changes, you must update the *modified* time of the linked pass so that Apple knows to send you a new pass. + +If you did not include the routes remember to configure APNSwift yourself like this: + +```swift +let apnsConfig: APNSClientConfiguration +if let pemPrivateKeyPassword { + apnsConfig = APNSClientConfiguration( + authenticationMethod: try .tls( + privateKey: .privateKey( + NIOSSLPrivateKey(file: privateKeyPath, format: .pem) { closure in + closure(pemPrivateKeyPassword.utf8) + }), + certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { .certificate($0) } + ), + environment: .production + ) +} else { + apnsConfig = APNSClientConfiguration( + authenticationMethod: try .tls( + privateKey: .file(privateKeyPath), + certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { .certificate($0) } + ), + environment: .production + ) +} +app.apns.containers.use( + apnsConfig, + eventLoopGroupProvider: .shared(app.eventLoopGroup), + responseDecoder: JSONDecoder(), + requestEncoder: JSONEncoder(), + as: .init(string: "passkit"), + isDefault: false +) +``` + #### Custom Implementation If you don't like the schema names that are used by default, you can instead instantiate the generic `PassKitCustom` and provide your model types. @@ -246,9 +256,51 @@ let pk = PassKitCustom Response { + ... + guard let passData = try await PassData.query(on: req.db) + .filter(...) + .with(\.$pass) + .first() + else { + throw Abort(.notFound) + } + + let bundle = try await passKit.generatePassContent(for: passData.pass, on: req.db).get() + let body = Response.Body(data: bundle) + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "application/vnd.apple.pkpass") + headers.add(name: .contentDisposition, value: "attachment; filename=pass.pkpass") // Add this header only if you are serving the pass in a web page + headers.add(name: .lastModified, value: String(passData.pass.modified.timeIntervalSince1970)) + headers.add(name: .contentTransferEncoding, value: "binary") + return Response(status: .ok, headers: headers, body: body) +} +``` diff --git a/Sources/PassKit/APNSwift+Extension.swift b/Sources/PassKit/APNSwift+Extension.swift deleted file mode 100644 index 24be72b..0000000 --- a/Sources/PassKit/APNSwift+Extension.swift +++ /dev/null @@ -1,46 +0,0 @@ -/// Copyright 2020 Gargoyle Software, LLC -/// -/// Permission is hereby granted, free of charge, to any person obtaining a copy -/// of this software and associated documentation files (the "Software"), to deal -/// in the Software without restriction, including without limitation the rights -/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -/// copies of the Software, and to permit persons to whom the Software is -/// furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, -/// distribute, sublicense, create a derivative work, and/or sell copies of the -/// Software in any work that is designed, intended, or marketed for pedagogical or -/// instructional purposes related to programming, coding, application development, -/// or information technology. Permission for such use, copying, modification, -/// merger, publication, distribution, sublicensing, creation of derivative works, -/// or sale is expressly withheld. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -/// THE SOFTWARE. - -import Vapor -import APNSwift -import NIOSSL - -// This will go away as soon as Kyle accepts my pull request -extension APNSwiftConfiguration { - public init(privateKeyPath: String, pemPath: String, topic: String, environment: APNSwiftConfiguration.Environment, - logger: Logger? = nil, passphraseCallback: @escaping NIOSSLPassphraseCallback) throws - where T.Element == UInt8 { - try self.init(keyIdentifier: "", teamIdentifier: "", signer: APNSwiftSigner(buffer: ByteBufferAllocator().buffer(capacity: 1024)), topic: topic, environment: environment, logger: logger) - let key = try NIOSSLPrivateKey(file: privateKeyPath, format: .pem, passphraseCallback: passphraseCallback) - self.tlsConfiguration.privateKey = NIOSSLPrivateKeySource.privateKey(key) - self.tlsConfiguration.certificateVerification = .noHostnameVerification - self.tlsConfiguration.certificateChain = try [.certificate(.init(file: pemPath, format: .pem))] - } -} - - diff --git a/Sources/PassKit/ApplePassMiddleware.swift b/Sources/PassKit/ApplePassMiddleware.swift index 9e8ca27..cd06789 100644 --- a/Sources/PassKit/ApplePassMiddleware.swift +++ b/Sources/PassKit/ApplePassMiddleware.swift @@ -28,16 +28,15 @@ import Vapor -struct ApplePassMiddleware: Middleware { +struct ApplePassMiddleware: AsyncMiddleware { let authorizationCode: String - func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { + func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { let auth = request.headers["Authorization"] guard auth.first == "ApplePass \(authorizationCode)" else { - return request.eventLoop.makeFailedFuture(Abort(.unauthorized)) + throw Abort(.unauthorized) } - - return next.respond(to: request) + return try await next.respond(to: request) } } diff --git a/Sources/PassKit/Models/ConcreteModels.swift b/Sources/PassKit/Models/ConcreteModels.swift index 80b24f4..abf8649 100644 --- a/Sources/PassKit/Models/ConcreteModels.swift +++ b/Sources/PassKit/Models/ConcreteModels.swift @@ -32,7 +32,7 @@ import Fluent final public class PKDevice: PassKitDevice { public static let schema = "devices" - @ID(key: "id") + @ID(custom: .id) public var id: Int? @Field(key: "push_token") @@ -49,25 +49,25 @@ final public class PKDevice: PassKitDevice { public init() {} } -extension PKDevice: Migration { - public func prepare(on database: Database) -> EventLoopFuture { - database.schema(Self.schema) - .field("id", .int, .identifier(auto: true)) +extension PKDevice: AsyncMigration { + public func prepare(on database: any Database) async throws { + try await database.schema(Self.schema) + .field(.id, .int, .identifier(auto: true)) .field("push_token", .string, .required) .field("device_library_identifier", .string, .required) .unique(on: "push_token", "device_library_identifier") .create() } - public func revert(on database: Database) -> EventLoopFuture { - database.schema(Self.schema).delete() + public func revert(on database: any Database) async throws { + try await database.schema(Self.schema).delete() } } open class PKPass: PassKitPass { public static let schema = "passes" - @ID(key: "id") + @ID public var id: UUID? @Field(key: "modified") @@ -81,24 +81,24 @@ open class PKPass: PassKitPass { } } -extension PKPass: Migration { - public func prepare(on database: Database) -> EventLoopFuture { - database.schema(Self.schema) - .field("id", .uuid, .identifier(auto: false)) +extension PKPass: AsyncMigration { + public func prepare(on database: any Database) async throws { + try await database.schema(Self.schema) + .id() .field("modified", .datetime, .required) .field("type_identifier", .string, .required) .create() } - public func revert(on database: Database) -> EventLoopFuture { - database.schema(Self.schema).delete() + public func revert(on database: any Database) async throws { + try await database.schema(Self.schema).delete() } } final public class PKErrorLog: PassKitErrorLog { public static let schema = "errors" - @ID(key: "id") + @ID(custom: .id) public var id: Int? @Field(key: "created") @@ -115,17 +115,17 @@ final public class PKErrorLog: PassKitErrorLog { public init() {} } -extension PKErrorLog: Migration { - public func prepare(on database: Database) -> EventLoopFuture { - database.schema(Self.schema) - .field("id", .int, .identifier(auto: true)) +extension PKErrorLog: AsyncMigration { + public func prepare(on database: any Database) async throws { + try await database.schema(Self.schema) + .field(.id, .int, .identifier(auto: true)) .field("created", .datetime, .required) .field("message", .string, .required) .create() } - public func revert(on database: Database) -> EventLoopFuture { - database.schema(PKErrorLog.schema).delete() + public func revert(on database: any Database) async throws { + try await database.schema(PKErrorLog.schema).delete() } } @@ -135,7 +135,7 @@ final public class PKRegistration: PassKitRegistration { public static let schema = "registrations" - @ID(key: "id") + @ID(custom: .id) public var id: Int? @Parent(key: "device_id") @@ -147,18 +147,18 @@ final public class PKRegistration: PassKitRegistration { public init() {} } -extension PKRegistration: Migration { - public func prepare(on database: Database) -> EventLoopFuture { - database.schema(Self.schema) - .field("id", .int, .identifier(auto: true)) +extension PKRegistration: AsyncMigration { + public func prepare(on database: any Database) async throws { + try await database.schema(Self.schema) + .field(.id, .int, .identifier(auto: true)) .field("device_id", .int, .required) .field("pass_id", .uuid, .required) - .foreignKey("device_id", references: PKDevice.schema, "id", onDelete: .cascade) - .foreignKey("pass_id", references: PKPass.schema, "id", onDelete: .cascade) + .foreignKey("device_id", references: PKDevice.schema, .id, onDelete: .cascade) + .foreignKey("pass_id", references: PKPass.schema, .id, onDelete: .cascade) .create() } - public func revert(on database: Database) -> EventLoopFuture { - database.schema(Self.schema).delete() + public func revert(on database: any Database) async throws { + try await database.schema(Self.schema).delete() } } diff --git a/Sources/PassKit/Models/PassKitRegistration.swift b/Sources/PassKit/Models/PassKitRegistration.swift index abcb008..6940eed 100644 --- a/Sources/PassKit/Models/PassKitRegistration.swift +++ b/Sources/PassKit/Models/PassKitRegistration.swift @@ -60,10 +60,10 @@ internal extension PassKitRegistration { return pass } - static func `for`(deviceLibraryIdentifier: String, passTypeIdentifier: String, on db: Database) -> QueryBuilder { + static func `for`(deviceLibraryIdentifier: String, passTypeIdentifier: String, on db: any Database) -> QueryBuilder { Self.query(on: db) - .join(\._$pass) - .join(\._$device) + .join(parent: \._$pass) + .join(parent: \._$device) .with(\._$pass) .with(\._$device) .filter(PassType.self, \._$type == passTypeIdentifier) diff --git a/Sources/PassKit/PassKit.swift b/Sources/PassKit/PassKit.swift index 0bf41ae..b6c7e14 100644 --- a/Sources/PassKit/PassKit.swift +++ b/Sources/PassKit/PassKit.swift @@ -28,28 +28,33 @@ import Vapor import APNS +import VaporAPNS +import APNSCore import Fluent +import NIOSSL public class PassKit { private let kit: PassKitCustom - public init(app: Application, delegate: PassKitDelegate, logger: Logger? = nil) { + public init(app: Application, delegate: any PassKitDelegate, logger: Logger? = nil) { kit = .init(app: app, delegate: delegate, logger: logger) } /// Registers all the routes required for PassKit to work. /// /// - Parameters: - /// - app: The `Application` passed to `routes(_:)` - /// - delegate: The `PassKitDelegate` to use. /// - authorizationCode: The `authenticationToken` which you are going to use in the `pass.json` file. public func registerRoutes(authorizationCode: String? = nil) { kit.registerRoutes(authorizationCode: authorizationCode) } - public func registerPushRoutes(middleware: Middleware) throws { + public func registerPushRoutes(middleware: any Middleware) throws { try kit.registerPushRoutes(middleware: middleware) } + + public func generatePassContent(for pass: PKPass, on db: any Database) -> EventLoopFuture { + kit.generatePassContent(for: pass, on: db) + } public static func register(migrations: Migrations) { migrations.add(PKPass()) @@ -58,16 +63,16 @@ public class PassKit { migrations.add(PKErrorLog()) } - public static func sendPushNotificationsForPass(id: UUID, of type: String, on db: Database, app: Application) -> EventLoopFuture { - PassKitCustom.sendPushNotificationsForPass(id: id, of: type, on: db, app: app) + public static func sendPushNotificationsForPass(id: UUID, of type: String, on db: any Database, app: Application) async throws { + try await PassKitCustom.sendPushNotificationsForPass(id: id, of: type, on: db, app: app) } - public static func sendPushNotifications(for pass: PKPass, on db: Database, app: Application) -> EventLoopFuture { - PassKitCustom.sendPushNotifications(for: pass, on: db, app: app) + public static func sendPushNotifications(for pass: PKPass, on db: any Database, app: Application) async throws { + try await PassKitCustom.sendPushNotifications(for: pass, on: db, app: app) } - public static func sendPushNotifications(for pass: Parent, on db: Database, app: Application) -> EventLoopFuture { - PassKitCustom.sendPushNotifications(for: pass, on: db, app: app) + public static func sendPushNotifications(for pass: ParentProperty, on db: any Database, app: Application) async throws { + try await PassKitCustom.sendPushNotifications(for: pass, on: db, app: app) } } @@ -79,14 +84,14 @@ public class PassKit { /// - Registration Type /// - Error Log Type public class PassKitCustom where P == R.PassType, D == R.DeviceType { - public unowned let delegate: PassKitDelegate + public unowned let delegate: any PassKitDelegate private unowned let app: Application private let processQueue = DispatchQueue(label: "com.vapor-community.PassKit", qos: .utility, attributes: .concurrent) - private let v1: RoutesBuilder + private let v1: any RoutesBuilder private let logger: Logger? - public init(app: Application, delegate: PassKitDelegate, logger: Logger? = nil) { + public init(app: Application, delegate: any PassKitDelegate, logger: Logger? = nil) { self.delegate = delegate self.logger = logger self.app = app @@ -97,8 +102,6 @@ public class PassKitCustom whe /// Registers all the routes required for PassKit to work. /// /// - Parameters: - /// - app: The `Application` passed to `routes(_:)` - /// - delegate: The `PassKitDelegate` to use. /// - authorizationCode: The `authenticationToken` which you are going to use in the `pass.json` file. public func registerRoutes(authorizationCode: String? = nil) { v1.get("devices", ":deviceLibraryIdentifier", "registrations", ":type", use: passesForDevice) @@ -125,7 +128,7 @@ public class PassKitCustom whe /// - Parameters: /// - middleware: The `Middleware` which will control authentication for the routes. /// - Throws: An error of type `PassKitError` - public func registerPushRoutes(middleware: Middleware) throws { + public func registerPushRoutes(middleware: any Middleware) throws { let privateKeyPath = URL(fileURLWithPath: delegate.pemPrivateKey, relativeTo: delegate.sslSigningFilesDirectory).unixPath() @@ -139,20 +142,36 @@ public class PassKitCustom whe throw PassKitError.pemCertificateMissing } - // PassKit *only* works with the production APNs. You can't pass in .sandbox here. - if app.apns.configuration == nil { - do { - if let pwd = delegate.pemPrivateKeyPassword { - app.apns.configuration = try .init(privateKeyPath: privateKeyPath, pemPath: pemPath, topic: "", environment: .production, logger: logger) { - $0(pwd.utf8) - } - } else { - app.apns.configuration = try .init(privateKeyPath: privateKeyPath, pemPath: pemPath, topic: "", environment: .production, logger: logger) - } - } catch { - throw PassKitError.nioPrivateKeyReadFailed(error) - } + // PassKit *only* works with the production APNs. You can't pass in .sandbox here. + let apnsConfig: APNSClientConfiguration + if let pwd = delegate.pemPrivateKeyPassword { + apnsConfig = APNSClientConfiguration( + authenticationMethod: try .tls( + privateKey: .privateKey( + NIOSSLPrivateKey(file: privateKeyPath, format: .pem) { closure in + closure(pwd.utf8) + }), + certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { .certificate($0) } + ), + environment: .production + ) + } else { + apnsConfig = APNSClientConfiguration( + authenticationMethod: try .tls( + privateKey: .file(privateKeyPath), + certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { .certificate($0) } + ), + environment: .production + ) } + app.apns.containers.use( + apnsConfig, + eventLoopGroupProvider: .shared(app.eventLoopGroup), + responseDecoder: JSONDecoder(), + requestEncoder: JSONEncoder(), + as: .init(string: "passkit"), + isDefault: false + ) let pushAuth = v1.grouped(middleware) @@ -161,7 +180,7 @@ public class PassKitCustom whe } // MARK: - API Routes - func registerDevice(req: Request) throws -> EventLoopFuture { + @Sendable func registerDevice(req: Request) async throws -> HTTPStatus { logger?.debug("Called register device") guard let serial = req.parameters.get("passSerial", as: UUID.self) else { @@ -179,31 +198,29 @@ public class PassKitCustom whe let type = req.parameters.get("type")! let deviceLibraryIdentifier = req.parameters.get("deviceLibraryIdentifier")! - return P.query(on: req.db) + guard let pass = try await P.query(on: req.db) .filter(\._$type == type) .filter(\._$id == serial) .first() - .unwrap(or: Abort(.notFound)) - .flatMap { pass in - D.query(on: req.db) - .filter(\._$deviceLibraryIdentifier == deviceLibraryIdentifier) - .filter(\._$pushToken == pushToken) - .first() - .flatMap { device in - if let device = device { - return Self.createRegistration(device: device, pass: pass, req: req) - } else { - let newDevice = D(deviceLibraryIdentifier: deviceLibraryIdentifier, pushToken: pushToken) - - return newDevice - .create(on: req.db) - .flatMap { _ in Self.createRegistration(device: newDevice, pass: pass, req: req) } - } - } + else { + throw Abort(.notFound) + } + + let device = try await D.query(on: req.db) + .filter(\._$deviceLibraryIdentifier == deviceLibraryIdentifier) + .filter(\._$pushToken == pushToken) + .first() + + if let device = device { + return try await Self.createRegistration(device: device, pass: pass, req: req) + } else { + let newDevice = D(deviceLibraryIdentifier: deviceLibraryIdentifier, pushToken: pushToken) + try await newDevice.create(on: req.db) + return try await Self.createRegistration(device: newDevice, pass: pass, req: req) } } - func passesForDevice(req: Request) throws -> EventLoopFuture { + @Sendable func passesForDevice(req: Request) async throws -> PassesForDeviceDto { logger?.debug("Called passesForDevice") let type = req.parameters.get("type")! @@ -216,30 +233,27 @@ public class PassKitCustom whe query = query.filter(P.self, \._$modified > when) } - return query - .all() - .flatMapThrowing { registrations in - guard !registrations.isEmpty else { - throw Abort(.noContent) - } - - var serialNumbers: [String] = [] - var maxDate = Date.distantPast - - registrations.forEach { r in - let pass = r.pass - - serialNumbers.append(pass.id!.uuidString) - if pass.modified > maxDate { - maxDate = pass.modified - } - } - - return PassesForDeviceDto(with: serialNumbers, maxDate: maxDate) + let registrations = try await query.all() + guard !registrations.isEmpty else { + throw Abort(.noContent) + } + + var serialNumbers: [String] = [] + var maxDate = Date.distantPast + + registrations.forEach { r in + let pass = r.pass + + serialNumbers.append(pass.id!.uuidString) + if pass.modified > maxDate { + maxDate = pass.modified + } } + + return PassesForDeviceDto(with: serialNumbers, maxDate: maxDate) } - func latestVersionOfPass(req: Request) throws -> EventLoopFuture { + @Sendable func latestVersionOfPass(req: Request) async throws -> Response { logger?.debug("Called latestVersionOfPass") guard FileManager.default.fileExists(atPath: delegate.zipBinary.unixPath()) else { @@ -256,32 +270,31 @@ public class PassKitCustom whe let id = req.parameters.get("passSerial", as: UUID.self) else { throw Abort(.badRequest) } - - return P.query(on: req.db) + + guard let pass = try await P.query(on: req.db) .filter(\._$id == id) .filter(\._$type == type) .first() - .unwrap(or: Abort(.notFound)) - .flatMap { pass in - guard ifModifiedSince < pass.modified.timeIntervalSince1970 else { - return req.eventLoop.makeFailedFuture(Abort(.notModified)) - } - - return self.generatePassContent(for: pass, on: req.db) - .map { data in - let body = Response.Body(data: data) - - var headers = HTTPHeaders() - headers.add(name: .contentType, value: "application/vnd.apple.pkpass") - headers.add(name: .lastModified, value: String(pass.modified.timeIntervalSince1970)) - headers.add(name: .contentTransferEncoding, value: "binary") - - return Response(status: .ok, headers: headers, body: body) - } + else { + throw Abort(.notFound) + } + + guard ifModifiedSince < pass.modified.timeIntervalSince1970 else { + throw Abort(.notModified) } + + let data = try await self.generatePassContent(for: pass, on: req.db).get() + let body = Response.Body(data: data) + + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "application/vnd.apple.pkpass") + headers.add(name: .lastModified, value: String(pass.modified.timeIntervalSince1970)) + headers.add(name: .contentTransferEncoding, value: "binary") + + return Response(status: .ok, headers: headers, body: body) } - func unregisterDevice(req: Request) throws -> EventLoopFuture { + @Sendable func unregisterDevice(req: Request) async throws -> HTTPStatus { logger?.debug("Called unregisterDevice") let type = req.parameters.get("type")! @@ -292,14 +305,17 @@ public class PassKitCustom whe let deviceLibraryIdentifier = req.parameters.get("deviceLibraryIdentifier")! - return R.for(deviceLibraryIdentifier: deviceLibraryIdentifier, passTypeIdentifier: type, on: req.db) + guard let r = try await R.for(deviceLibraryIdentifier: deviceLibraryIdentifier, passTypeIdentifier: type, on: req.db) .filter(P.self, \._$id == passId) .first() - .unwrap(or: Abort(.notFound)) - .flatMap { $0.delete(on: req.db).map { .ok } } + else { + throw Abort(.notFound) + } + try await r.delete(on: req.db) + return .ok } - func logError(req: Request) throws -> EventLoopFuture { + @Sendable func logError(req: Request) throws -> EventLoopFuture { logger?.debug("Called logError") let body: ErrorLogDto @@ -320,7 +336,7 @@ public class PassKitCustom whe .map { .ok } } - func pushUpdatesForPass(req: Request) throws -> EventLoopFuture { + @Sendable func pushUpdatesForPass(req: Request) async throws -> HTTPStatus { logger?.debug("Called pushUpdatesForPass") guard let id = req.parameters.get("passSerial", as: UUID.self) else { @@ -329,11 +345,11 @@ public class PassKitCustom whe let type = req.parameters.get("type")! - return Self.sendPushNotificationsForPass(id: id, of: type, on: req.db, app: req.application) - .map { _ in .noContent } + try await Self.sendPushNotificationsForPass(id: id, of: type, on: req.db, app: req.application) + return .noContent } - func tokensForPassUpdate(req: Request) throws -> EventLoopFuture<[String]> { + @Sendable func tokensForPassUpdate(req: Request) async throws -> [String] { logger?.debug("Called tokensForPassUpdate") guard let id = req.parameters.get("passSerial", as: UUID.self) else { @@ -342,84 +358,71 @@ public class PassKitCustom whe let type = req.parameters.get("type")! - return Self.registrationsForPass(id: id, of: type, on: req.db).map { $0.map { $0.device.pushToken } } + let registrations = try await Self.registrationsForPass(id: id, of: type, on: req.db) + return registrations.map { $0.device.pushToken } } - private static func createRegistration(device: D, pass: P, req: Request) -> EventLoopFuture { - R.for(deviceLibraryIdentifier: device.deviceLibraryIdentifier, passTypeIdentifier: pass.type, on: req.db) + private static func createRegistration(device: D, pass: P, req: Request) async throws -> HTTPStatus { + let r = try await R.for(deviceLibraryIdentifier: device.deviceLibraryIdentifier, passTypeIdentifier: pass.type, on: req.db) .filter(P.self, \._$id == pass.id!) .first() - .flatMap { r in - if r != nil { - // If the registration already exists, docs say to return a 200 - return req.eventLoop.makeSucceededFuture(.ok) - } - - let registration = R() - registration._$pass.id = pass.id! - registration._$device.id = device.id! - - return registration.create(on: req.db) - .map { .created } + if r != nil { + // If the registration already exists, docs say to return a 200 + return .ok } + + let registration = R() + registration._$pass.id = pass.id! + registration._$device.id = device.id! + + try await registration.create(on: req.db) + return .created } // MARK: - Push Notifications - public static func sendPushNotificationsForPass(id: UUID, of type: String, on db: Database, app: Application) -> EventLoopFuture { - Self.registrationsForPass(id: id, of: type, on: db) - .flatMap { - $0.map { reg in - let payload = "{}".data(using: .utf8)! - var rawBytes = ByteBufferAllocator().buffer(capacity: payload.count) - rawBytes.writeBytes(payload) - - return app.apns.send(rawBytes: rawBytes, pushType: .background, to: reg.device.pushToken, topic: reg.pass.type) - .flatMapError { - // Unless APNs said it was a bad device token, just ignore the error. - guard case let APNSwiftError.ResponseError.badRequest(response) = $0, response == .badDeviceToken else { - return db.eventLoop.future() - } - - // Be sure the device deletes before the registration is deleted. - // If you let them run in parallel issues might arise depending on - // the hooks people have set for when a registration deletes, as it - // might try to delete the same device again. - return reg.device.delete(on: db) - .flatMapError { _ in db.eventLoop.future() } - .flatMap { reg.delete(on: db) } - } - } - .flatten(on: db.eventLoop) + public static func sendPushNotificationsForPass(id: UUID, of type: String, on db: any Database, app: Application) async throws { + let registrations = try await Self.registrationsForPass(id: id, of: type, on: db) + for reg in registrations { + let backgroundNotification = APNSBackgroundNotification(expiration: .immediately, topic: reg.pass.type, payload: EmptyPayload()) + do { + try await app.apns.client(.init(string: "passkit")).sendBackgroundNotification( + backgroundNotification, + deviceToken: reg.device.pushToken + ) + } catch let error as APNSCore.APNSError where error.reason == .badDeviceToken { + try await reg.device.delete(on: db) + try await reg.delete(on: db) + } } } - - public static func sendPushNotifications(for pass: P, on db: Database, app: Application) -> EventLoopFuture { + + public static func sendPushNotifications(for pass: P, on db: any Database, app: Application) async throws { guard let id = pass.id else { - return db.eventLoop.makeFailedFuture(FluentError.idRequired) + throw FluentError.idRequired } - return Self.sendPushNotificationsForPass(id: id, of: pass.type, on: db, app: app) + try await Self.sendPushNotificationsForPass(id: id, of: pass.type, on: db, app: app) } - public static func sendPushNotifications(for pass: Parent

, on db: Database, app: Application) -> EventLoopFuture { - let future: EventLoopFuture

+ public static func sendPushNotifications(for pass: ParentProperty, on db: any Database, app: Application) async throws { + let value: P - if let eagerLoaded = pass.eagerLoaded { - future = db.eventLoop.makeSucceededFuture(eagerLoaded) + if let eagerLoaded = pass.value { + value = eagerLoaded } else { - future = pass.get(on: db) + value = try await pass.get(on: db) } - return future.flatMap { sendPushNotifications(for: $0, on: db, app: app) } + try await sendPushNotifications(for: value, on: db, app: app) } - private static func registrationsForPass(id: UUID, of type: String, on db: Database) -> EventLoopFuture<[R]> { + private static func registrationsForPass(id: UUID, of type: String, on db: any Database) async throws -> [R] { // This could be done by enforcing the caller to have a Siblings property // wrapper, but there's not really any value to forcing that on them when // we can just do the query ourselves like this. - R.query(on: db) - .join(\._$pass) - .join(\._$device) + try await R.query(on: db) + .join(parent: \._$pass) + .join(parent: \._$device) .with(\._$pass) .with(\._$device) .filter(P.self, \._$type == type) @@ -440,7 +443,7 @@ public class PassKitCustom whe let data = try Data(contentsOf: file) let hash = Insecure.SHA1.hash(data: data) - manifest[relativePath] = hash.description + manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() } let encoded = try encoder.encode(manifest) @@ -498,7 +501,7 @@ public class PassKitCustom whe proc.waitUntilExit() } - private func generatePassContent(for pass: P, on db: Database) -> EventLoopFuture { + public func generatePassContent(for pass: P, on db: any Database) -> EventLoopFuture { let tmp = FileManager.default.temporaryDirectory let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true) let zipFile = tmp.appendingPathComponent("\(UUID().uuidString).zip") diff --git a/Sources/PassKit/PassKitDelegate.swift b/Sources/PassKit/PassKitDelegate.swift index 7de07f7..2f4f820 100644 --- a/Sources/PassKit/PassKitDelegate.swift +++ b/Sources/PassKit/PassKitDelegate.swift @@ -29,7 +29,7 @@ import Vapor import Fluent -public protocol PassKitDelegate: class { +public protocol PassKitDelegate: AnyObject { /// Should return a `URL` which points to the template data for the pass. /// /// The URL should point to a directory containing all the images and @@ -43,7 +43,7 @@ public protocol PassKitDelegate: class { /// /// ### Note ### /// Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor. - func template(for: P, db: Database) -> EventLoopFuture + func template(for: P, db: any Database) -> EventLoopFuture /// Generates the SSL `signature` file. /// @@ -64,7 +64,7 @@ public protocol PassKitDelegate: class { /// - db: The SQL database to query against. /// - encoder: The `JSONEncoder` which you should use. /// - See: [Understanding the Keys](https://developer.apple.com/library/archive/documentation/UserExperience/Reference/PassKit_Bundle/Chapters/Introduction.html) - func encode(pass: P, db: Database, encoder: JSONEncoder) -> EventLoopFuture + func encode(pass: P, db: any Database, encoder: JSONEncoder) -> EventLoopFuture /// Should return a `URL` which points to the template data for the pass. /// @@ -114,7 +114,7 @@ public extension PassKitDelegate { } var pemPrivateKey: String { - get { return "passkey.pkey" } + get { return "passkey.pem" } } var pemPrivateKeyPassword: String? { diff --git a/Sources/PassKit/PassKitError.swift b/Sources/PassKit/PassKitError.swift index 1aef5ef..1828a39 100644 --- a/Sources/PassKit/PassKitError.swift +++ b/Sources/PassKit/PassKitError.swift @@ -18,7 +18,7 @@ public enum PassKitError: Error { case pemPrivateKeyMissing /// Swift NIO failed to read the key. - case nioPrivateKeyReadFailed(Error) + case nioPrivateKeyReadFailed(any Error) /// The path to the zip binary is incorrect. case zipBinaryMissing diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index a92a9b8..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import PassKitTests - -var tests = [XCTestCaseEntry]() -tests += PassKitTests.allTests() -XCTMain(tests) diff --git a/Tests/PassKitTests/XCTestManifests.swift b/Tests/PassKitTests/XCTestManifests.swift deleted file mode 100644 index 99f1be1..0000000 --- a/Tests/PassKitTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(PassKitTests.allTests), - ] -} -#endif