From a6d96ecf41865ba2e9e3ca475091443e33f0f67f Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Tue, 25 Jun 2024 23:15:30 +0200 Subject: [PATCH] Update product and model names (#6) * Update package to Swift 5.10 * Change the product name to `Passes` in preparation for the addition of the `Orders` module * Update names of structs and properties to better represent what they are * Make `updatedAt` and `createdAt` properties actual `@Timestamp`s * Update `README.md` * Improve DooC and add `.spi.yml` --- .spi.yml | 4 + Package.swift | 22 ++- README.md | 106 ++++++++++----- .../DTOs/ErrorLogDTO.swift} | 2 +- .../DTOs/PassesForDeviceDTO.swift} | 3 +- .../DTOs/RegistrationDTO.swift} | 2 +- .../{PassKit => Passes}/FakeSendable.swift | 0 .../Middleware}/ApplePassMiddleware.swift | 0 .../Models/ConcreteModels.swift | 39 +++--- .../Models/PassKitDevice.swift | 0 .../Models/PassKitErrorLog.swift | 0 .../Models/PassKitPass.swift | 28 ++-- .../Models/PassKitPassData.swift | 1 + .../Models/PassKitRegistration.swift | 2 +- .../{PassKit => Passes}/PassKitError.swift | 0 .../PassKit.swift => Passes/Passes.swift} | 128 +++++++++++------- .../PassesDelegate.swift} | 5 +- .../URL+Extension.swift} | 0 Tests/PassKitTests/PassKitTests.swift | 4 +- 19 files changed, 212 insertions(+), 134 deletions(-) create mode 100644 .spi.yml rename Sources/{PassKit/DTO/ErrorLogDto.swift => Passes/DTOs/ErrorLogDTO.swift} (98%) rename Sources/{PassKit/DTO/PassesForDeviceDto.swift => Passes/DTOs/PassesForDeviceDTO.swift} (98%) rename Sources/{PassKit/DTO/RegistrationDto.swift => Passes/DTOs/RegistrationDTO.swift} (98%) rename Sources/{PassKit => Passes}/FakeSendable.swift (100%) rename Sources/{PassKit => Passes/Middleware}/ApplePassMiddleware.swift (100%) rename Sources/{PassKit => Passes}/Models/ConcreteModels.swift (80%) rename Sources/{PassKit => Passes}/Models/PassKitDevice.swift (100%) rename Sources/{PassKit => Passes}/Models/PassKitErrorLog.swift (100%) rename Sources/{PassKit => Passes}/Models/PassKitPass.swift (75%) rename Sources/{PassKit => Passes}/Models/PassKitPassData.swift (96%) rename Sources/{PassKit => Passes}/Models/PassKitRegistration.swift (97%) rename Sources/{PassKit => Passes}/PassKitError.swift (100%) rename Sources/{PassKit/PassKit.swift => Passes/Passes.swift} (78%) rename Sources/{PassKit/PassKitDelegate.swift => Passes/PassesDelegate.swift} (97%) rename Sources/{PassKit/Url+Extension.swift => Passes/URL+Extension.swift} (100%) diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..bb99af5 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [Passes] \ No newline at end of file diff --git a/Package.swift b/Package.swift index 9c405bf..63291a0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ -// swift-tools-version:5.9 - +// swift-tools-version:5.10 import PackageDescription let package = Package( @@ -8,33 +7,30 @@ let package = Package( .macOS(.v13), .iOS(.v16) ], products: [ - .library(name: "PassKit", targets: ["PassKit"]), + .library(name: "Passes", targets: ["Passes"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.92.5"), - .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.102.0"), + .package(url: "https://github.com/vapor/fluent.git", from: "4.11.0"), .package(url: "https://github.com/vapor/apns.git", from: "4.1.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4") + .package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"), ], targets: [ .target( - name: "PassKit", + name: "Passes", dependencies: [ .product(name: "Fluent", package: "fluent"), .product(name: "Vapor", package: "vapor"), .product(name: "VaporAPNS", package: "apns"), - .product(name: "Logging", package: "swift-log") + .product(name: "Logging", package: "swift-log"), ], swiftSettings: swiftSettings ), .testTarget( name: "PassKitTests", 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") + .target(name: "Passes"), + .product(name: "XCTVapor", package: "vapor"), ], swiftSettings: swiftSettings ), diff --git a/README.md b/README.md index 7e89a0b..53450d2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,32 @@ # PassKit [![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 Apple Wallet. +🎟️ A Vapor package which handles all the server side elements required to implement passes for Apple Wallet. -## NOTE +### Major Releases -This package requires Vapor 4. +The table below shows a list of PassKit major releases alongside their compatible Swift versions. + +|Version|Swift|SPM| +|---|---|---| +|0.3.0|5.10+|`from: "0.3.0"`| +|0.2.0|5.9+|`from: "0.2.0"`| +|0.1.0|5.9+|`from: "0.1.0"`| + +Use the SPM string to easily include the dependendency in your `Package.swift` file + +```swift +.package(url: "https://github.com/vapor-community/PassKit.git", from: "0.3.0") +``` + +and add it to your target's dependencies: + +```swift +.product(name: "Passes", package: "PassKit") +``` + +> Note: This package requires Vapor 4. ## Usage @@ -16,7 +35,11 @@ This package requires Vapor 4. 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 -final class PassData: PassKitPassData { +import Fluent +import struct Foundation.UUID +import Passes + +final class PassData: PassKitPassData, @unchecked Sendable { static let schema = "pass_data" @ID @@ -29,7 +52,7 @@ final class PassData: PassKitPassData { @Field(key: "punches") var punches: Int - init() {} + init() { } } struct CreatePassData: AsyncMigration { @@ -41,7 +64,7 @@ struct CreatePassData: AsyncMigration { .create() } - public func revert(on database: Database) -> EventLoopFuture { + public func revert(on database: Database) async throws { try await database.schema(Self.schema).delete() } } @@ -91,7 +114,7 @@ For information on the various keys available see the [documentation](https://de 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 { +struct PassJSONData: Encodable { public static let token = "EB80D9C6-AD37-41A0-875E-3802E88CA478" private let formatVersion = 1 @@ -99,7 +122,7 @@ struct PassJsonData: Encodable { private let authenticationToken = token let serialNumber: String let relevantDate: String - let barcodes: [PassJsonData.Barcode] + let barcodes: [PassJSONData.Barcode] ... struct Barcode: Encodable { @@ -117,7 +140,7 @@ struct PassJsonData: Encodable { ### Implement the delegate. -Create a delegate file that implements `PassKitDelegate`. +Create a delegate file that implements `PassesDelegate`. 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. Obtaining the three certificates files could be a bit tricky, you could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). There are other fields available which have reasonable default values. See the delegate's documentation. @@ -126,10 +149,10 @@ Because the files for your pass' template and the method of encoding might vary ```swift import Vapor import Fluent -import PassKit +import Passes -final class PKDelegate: PassKitDelegate { - let sslSigningFilesDirectory = URL(fileURLWithPath: "/www/myapp/sign", isDirectory: true) +final class PKDelegate: PassesDelegate { + let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/", isDirectory: true) let pemPrivateKeyPassword: String? = Environment.get("PEM_PRIVATE_KEY_PASSWORD")! @@ -142,7 +165,7 @@ final class PKDelegate: PassKitDelegate { else { throw Abort(.internalServerError) } - guard let data = try? encoder.encode(PassJsonData(data: passData, pass: pass)) else { + guard let data = try? encoder.encode(PassJSONData(data: passData, pass: pass)) else { throw Abort(.internalServerError) } return data @@ -150,7 +173,7 @@ final class PKDelegate: PassKitDelegate { func template(for: P, db: Database) async throws -> URL { // The location might vary depending on the type of pass. - return URL(fileURLWithPath: "/www/myapp/pass", isDirectory: true) + return URL(fileURLWithPath: "PassKitTemplate/", isDirectory: true) } } ``` @@ -164,11 +187,14 @@ a global variable. You need to ensure that the delegate doesn't go out of scope This will implement all of the routes that PassKit expects to exist on your server for you. ```swift -let delegate = PKDelegate() +import Vapor +import Passes + +let pkDelegate = PKDelegate() func routes(_ app: Application) throws { - let pk = PassKit(app: app, delegate: delegate) - pk.registerRoutes(authorizationCode: PassData.token) + let passes = Passes(app: app, delegate: pkDelegate) + passes.registerRoutes(authorizationCode: PassJSONData.token) } ``` @@ -177,7 +203,7 @@ func routes(_ app: Application) throws { 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: SecretMiddleware()) +try passes.registerPushRoutes(middleware: SecretMiddleware(secret: "foo")) ``` That will add two routes: @@ -185,9 +211,13 @@ 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 middleware that sends push notifications and updates the `modified` field when your pass data updates. You can implement it like so: +Whether you include the routes or not, you'll want to add a model middleware that sends push notifications and updates the `updatedAt` field when your pass data updates. The model middleware could also create and link the `PKPass` during the creation of the pass data, depending on your requirements. You can implement it like so: ```swift +import Vapor +import Fluent +import Passes + struct PassDataMiddleware: AsyncModelMiddleware { private unowned let app: Application @@ -195,12 +225,20 @@ struct PassDataMiddleware: AsyncModelMiddleware { self.app = app } + // Create the PKPass and add it to the PassData automatically at creation + func create(model: PassData, on db: Database, next: AnyAsyncModelResponder) async throws { + let pkPass = PKPass(passTypeIdentifier: "pass.com.yoursite.passType") + try await pkPass.save(on: db) + model.$pass.id = try pkPass.requireID() + try await next.create(model, on: db) + } + 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) + pkPass.updatedAt = Date() + try await pkPass.save(on: db) try await next.update(model, on: db) - try await PassKit.sendPushNotifications(for: model.$pass.get(on: db), on: db, app: self.app) + try await Passes.sendPushNotifications(for: pkPass, on: db, app: self.app) } } ``` @@ -211,7 +249,8 @@ 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. +> [!IMPORTANT] +> Whenever your pass data changes, you must update the *updatedAt* 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: @@ -242,17 +281,17 @@ app.apns.containers.use( eventLoopGroupProvider: .shared(app.eventLoopGroup), responseDecoder: JSONDecoder(), requestEncoder: JSONEncoder(), - as: .init(string: "passkit"), + as: .init(string: "passes"), 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. +If you don't like the schema names that are used by default, you can instead instantiate the generic `PassesCustom` and provide your model types. ```swift -let pk = PassKitCustom(app: app, delegate: delegate) +let passes = PassesCustom(app: app, delegate: delegate) ``` ### Register Migrations @@ -260,21 +299,22 @@ let pk = PassKitCustom Response { throw Abort(.notFound) } - let bundle = try await passKit.generatePassContent(for: passData.pass, on: req.db) + let bundle = try await passes.generatePassContent(for: passData.pass, on: req.db) 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: .lastModified, value: String(passData.pass.updatedAt?.timeIntervalSince1970 ?? 0)) headers.add(name: .contentTransferEncoding, value: "binary") return Response(status: .ok, headers: headers, body: body) } diff --git a/Sources/PassKit/DTO/ErrorLogDto.swift b/Sources/Passes/DTOs/ErrorLogDTO.swift similarity index 98% rename from Sources/PassKit/DTO/ErrorLogDto.swift rename to Sources/Passes/DTOs/ErrorLogDTO.swift index 6d7d33c..a1af6f7 100644 --- a/Sources/PassKit/DTO/ErrorLogDto.swift +++ b/Sources/Passes/DTOs/ErrorLogDTO.swift @@ -28,6 +28,6 @@ import Vapor -struct ErrorLogDto: Content { +struct ErrorLogDTO: Content { let logs: [String] } diff --git a/Sources/PassKit/DTO/PassesForDeviceDto.swift b/Sources/Passes/DTOs/PassesForDeviceDTO.swift similarity index 98% rename from Sources/PassKit/DTO/PassesForDeviceDto.swift rename to Sources/Passes/DTOs/PassesForDeviceDTO.swift index 2e3451f..8a17e3a 100644 --- a/Sources/PassKit/DTO/PassesForDeviceDto.swift +++ b/Sources/Passes/DTOs/PassesForDeviceDTO.swift @@ -28,7 +28,7 @@ import Vapor -struct PassesForDeviceDto: Content { +struct PassesForDeviceDTO: Content { let lastUpdated: String let serialNumbers: [String] @@ -36,5 +36,4 @@ struct PassesForDeviceDto: Content { lastUpdated = String(maxDate.timeIntervalSince1970) self.serialNumbers = serialNumbers } - } diff --git a/Sources/PassKit/DTO/RegistrationDto.swift b/Sources/Passes/DTOs/RegistrationDTO.swift similarity index 98% rename from Sources/PassKit/DTO/RegistrationDto.swift rename to Sources/Passes/DTOs/RegistrationDTO.swift index ed63af2..a0a2068 100644 --- a/Sources/PassKit/DTO/RegistrationDto.swift +++ b/Sources/Passes/DTOs/RegistrationDTO.swift @@ -28,6 +28,6 @@ import Vapor -struct RegistrationDto: Content { +struct RegistrationDTO: Content { let pushToken: String } diff --git a/Sources/PassKit/FakeSendable.swift b/Sources/Passes/FakeSendable.swift similarity index 100% rename from Sources/PassKit/FakeSendable.swift rename to Sources/Passes/FakeSendable.swift diff --git a/Sources/PassKit/ApplePassMiddleware.swift b/Sources/Passes/Middleware/ApplePassMiddleware.swift similarity index 100% rename from Sources/PassKit/ApplePassMiddleware.swift rename to Sources/Passes/Middleware/ApplePassMiddleware.swift diff --git a/Sources/PassKit/Models/ConcreteModels.swift b/Sources/Passes/Models/ConcreteModels.swift similarity index 80% rename from Sources/PassKit/Models/ConcreteModels.swift rename to Sources/Passes/Models/ConcreteModels.swift index abf8649..f6c7bea 100644 --- a/Sources/PassKit/Models/ConcreteModels.swift +++ b/Sources/Passes/Models/ConcreteModels.swift @@ -29,7 +29,8 @@ import Vapor import Fluent -final public class PKDevice: PassKitDevice { +/// The `Model` that stores PassKit devices. +final public class PKDevice: PassKitDevice, @unchecked Sendable { public static let schema = "devices" @ID(custom: .id) @@ -64,20 +65,23 @@ extension PKDevice: AsyncMigration { } } -open class PKPass: PassKitPass { +/// The `Model` that stores PassKit passes. +open class PKPass: PassKitPass, @unchecked Sendable { public static let schema = "passes" @ID public var id: UUID? - @Field(key: "modified") - public var modified: Date + @Timestamp(key: "updated_at", on: .update) + public var updatedAt: Date? - @Field(key: "type_identifier") - public var type: String + @Field(key: "pass_type_identifier") + public var passTypeIdentifier: String + + public required init() { } - public required init() { - self.modified = Date() + public required init(passTypeIdentifier: String) { + self.passTypeIdentifier = passTypeIdentifier } } @@ -85,8 +89,8 @@ 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) + .field("updated_at", .datetime, .required) + .field("pass_type_identifier", .string, .required) .create() } @@ -95,20 +99,20 @@ extension PKPass: AsyncMigration { } } -final public class PKErrorLog: PassKitErrorLog { +/// The `Model` that stores PassKit error logs. +final public class PKErrorLog: PassKitErrorLog, @unchecked Sendable { public static let schema = "errors" @ID(custom: .id) public var id: Int? - @Field(key: "created") - public var date: Date + @Timestamp(key: "created_at", on: .create) + public var createdAt: Date? @Field(key: "message") public var message: String public init(message: String) { - date = Date() self.message = message } @@ -129,7 +133,8 @@ extension PKErrorLog: AsyncMigration { } } -final public class PKRegistration: PassKitRegistration { +/// The `Model` that stores PassKit registrations. +final public class PKRegistration: PassKitRegistration, @unchecked Sendable { public typealias PassType = PKPass public typealias DeviceType = PKDevice @@ -153,8 +158,8 @@ extension PKRegistration: AsyncMigration { .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: DeviceType.schema, .id, onDelete: .cascade) + .foreignKey("pass_id", references: PassType.schema, .id, onDelete: .cascade) .create() } diff --git a/Sources/PassKit/Models/PassKitDevice.swift b/Sources/Passes/Models/PassKitDevice.swift similarity index 100% rename from Sources/PassKit/Models/PassKitDevice.swift rename to Sources/Passes/Models/PassKitDevice.swift diff --git a/Sources/PassKit/Models/PassKitErrorLog.swift b/Sources/Passes/Models/PassKitErrorLog.swift similarity index 100% rename from Sources/PassKit/Models/PassKitErrorLog.swift rename to Sources/Passes/Models/PassKitErrorLog.swift diff --git a/Sources/PassKit/Models/PassKitPass.swift b/Sources/Passes/Models/PassKitPass.swift similarity index 75% rename from Sources/PassKit/Models/PassKitPass.swift rename to Sources/Passes/Models/PassKitPass.swift index 9a7e29a..6f4e174 100644 --- a/Sources/PassKit/Models/PassKitPass.swift +++ b/Sources/Passes/Models/PassKitPass.swift @@ -29,13 +29,13 @@ import Vapor import Fluent -/// Represents the `Model` that stores PassKit passes.. Uses a UUID so people can't easily guess your pass IDs +/// Represents the `Model` that stores PassKit passes. Uses a UUID so people can't easily guess pass IDs public protocol PassKitPass: Model where IDValue == UUID { - /// The pass type - var type: String { get set } + /// The pass type identifier. + var passTypeIdentifier: String { get set } /// The last time the pass was modified. - var modified: Date { get set } + var updatedAt: Date? { get set } } internal extension PassKitPass { @@ -48,21 +48,21 @@ internal extension PassKitPass { return id } - var _$type: Field { - guard let mirror = Mirror(reflecting: self).descendant("_type"), - let type = mirror as? Field else { - fatalError("type property must be declared using @Field") + var _$passTypeIdentifier: Field { + guard let mirror = Mirror(reflecting: self).descendant("_passTypeIdentifier"), + let passTypeIdentifier = mirror as? Field else { + fatalError("passTypeIdentifier property must be declared using @Field") } - return type + return passTypeIdentifier } - var _$modified: Field { - guard let mirror = Mirror(reflecting: self).descendant("_modified"), - let modified = mirror as? Field else { - fatalError("modified property must be declared using @Field") + var _$updatedAt: Timestamp { + guard let mirror = Mirror(reflecting: self).descendant("_updatedAt"), + let updatedAt = mirror as? Timestamp else { + fatalError("updatedAt property must be declared using @Timestamp(on: .update)") } - return modified + return updatedAt } } diff --git a/Sources/PassKit/Models/PassKitPassData.swift b/Sources/Passes/Models/PassKitPassData.swift similarity index 96% rename from Sources/PassKit/Models/PassKitPassData.swift rename to Sources/Passes/Models/PassKitPassData.swift index 68f470b..cf419a2 100644 --- a/Sources/PassKit/Models/PassKitPassData.swift +++ b/Sources/Passes/Models/PassKitPassData.swift @@ -29,6 +29,7 @@ import Vapor import Fluent +/// Represents the `Model` that stores custom app data associated to PassKit passes. public protocol PassKitPassData: Model { associatedtype PassType: PassKitPass diff --git a/Sources/PassKit/Models/PassKitRegistration.swift b/Sources/Passes/Models/PassKitRegistration.swift similarity index 97% rename from Sources/PassKit/Models/PassKitRegistration.swift rename to Sources/Passes/Models/PassKitRegistration.swift index 6940eed..18c53ac 100644 --- a/Sources/PassKit/Models/PassKitRegistration.swift +++ b/Sources/Passes/Models/PassKitRegistration.swift @@ -66,7 +66,7 @@ internal extension PassKitRegistration { .join(parent: \._$device) .with(\._$pass) .with(\._$device) - .filter(PassType.self, \._$type == passTypeIdentifier) + .filter(PassType.self, \._$passTypeIdentifier == passTypeIdentifier) .filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier) } } diff --git a/Sources/PassKit/PassKitError.swift b/Sources/Passes/PassKitError.swift similarity index 100% rename from Sources/PassKit/PassKitError.swift rename to Sources/Passes/PassKitError.swift diff --git a/Sources/PassKit/PassKit.swift b/Sources/Passes/Passes.swift similarity index 78% rename from Sources/PassKit/PassKit.swift rename to Sources/Passes/Passes.swift index 662c707..13277ce 100644 --- a/Sources/PassKit/PassKit.swift +++ b/Sources/Passes/Passes.swift @@ -33,10 +33,11 @@ import VaporAPNS import Fluent import NIOSSL -public final class PassKit: Sendable { - private let kit: PassKitCustom +/// The main class that handles PassKit passes. +public final class Passes: Sendable { + private let kit: PassesCustom - public init(app: Application, delegate: any PassKitDelegate, logger: Logger? = nil) { + public init(app: Application, delegate: any PassesDelegate, logger: Logger? = nil) { kit = .init(app: app, delegate: delegate, logger: logger) } @@ -47,14 +48,26 @@ public final class PassKit: Sendable { kit.registerRoutes(authorizationCode: authorizationCode) } + /// Registers routes to send push notifications to updated passes. + /// + /// - Parameter middleware: The `Middleware` which will control authentication for the routes. public func registerPushRoutes(middleware: any Middleware) throws { try kit.registerPushRoutes(middleware: middleware) } + /// Generates the pass content bundle for a given pass. + /// + /// - Parameters: + /// - pass: The pass to generate the content for. + /// - db: The `Database` to use. + /// - Returns: The generated pass content. public func generatePassContent(for pass: PKPass, on db: any Database) async throws -> Data { try await kit.generatePassContent(for: pass, on: db) } + /// Adds the migrations for PassKit passes models. + /// + /// - Parameter migrations: The `Migrations` object to add the migrations to. public static func register(migrations: Migrations) { migrations.add(PKPass()) migrations.add(PKDevice()) @@ -62,35 +75,54 @@ public final class PassKit: Sendable { migrations.add(PKErrorLog()) } - 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) + /// Sends push notifications for a given pass. + /// + /// - Parameters: + /// - id: The `UUID` of the pass to send the notifications for. + /// - passTypeIdentifier: The type identifier of the pass. + /// - db: The `Database` to use. + /// - app: The `Application` to use. + public static func sendPushNotificationsForPass(id: UUID, of passTypeIdentifier: String, on db: any Database, app: Application) async throws { + try await PassesCustom.sendPushNotificationsForPass(id: id, of: passTypeIdentifier, on: db, app: app) } + /// Sends push notifications for a given pass. + /// + /// - Parameters: + /// - pass: The pass to send the notifications for. + /// - db: The `Database` to use. + /// - app: The `Application` to use. 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) + try await PassesCustom.sendPushNotifications(for: pass, on: db, app: app) } + /// Sends push notifications for a given pass. + /// + /// - Parameters: + /// - pass: The pass (as the `ParentProperty`) to send the notifications for. + /// - db: The `Database` to use. + /// - app: The `Application` to use. 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) + try await PassesCustom.sendPushNotifications(for: pass, on: db, app: app) } } -/// Class to handle PassKit. +/// Class to handle `Passes`. /// /// The generics should be passed in this order: /// - Pass Type /// - Device Type /// - Registration Type /// - Error Log Type -public final class PassKitCustom: Sendable where P == R.PassType, D == R.DeviceType { - public unowned let delegate: any PassKitDelegate +public final class PassesCustom: Sendable where P == R.PassType, D == R.DeviceType { + public unowned let delegate: any PassesDelegate private unowned let app: Application private let processQueue = DispatchQueue(label: "com.vapor-community.PassKit", qos: .utility, attributes: .concurrent) private let v1: FakeSendable private let logger: Logger? - public init(app: Application, delegate: any PassKitDelegate, logger: Logger? = nil) { + public init(app: Application, delegate: any PassesDelegate, logger: Logger? = nil) { self.delegate = delegate self.logger = logger self.app = app @@ -102,7 +134,7 @@ public final class PassKitCustom PassesForDeviceDto { + func passesForDevice(req: Request) async throws -> PassesForDeviceDTO { logger?.debug("Called passesForDevice") - let type = req.parameters.get("type")! + let passTypeIdentifier = req.parameters.get("passTypeIdentifier")! let deviceLibraryIdentifier = req.parameters.get("deviceLibraryIdentifier")! - var query = R.for(deviceLibraryIdentifier: deviceLibraryIdentifier, passTypeIdentifier: type, on: req.db) + var query = R.for(deviceLibraryIdentifier: deviceLibraryIdentifier, passTypeIdentifier: passTypeIdentifier, on: req.db) if let since: TimeInterval = req.query["passesUpdatedSince"] { let when = Date(timeIntervalSince1970: since) - query = query.filter(P.self, \._$modified > when) + query = query.filter(P.self, \._$updatedAt > when) } let registrations = try await query.all() @@ -242,12 +274,12 @@ public final class PassKitCustom maxDate { - maxDate = pass.modified + if let updatedAt = pass.updatedAt, updatedAt > maxDate { + maxDate = updatedAt } } - return PassesForDeviceDto(with: serialNumbers, maxDate: maxDate) + return PassesForDeviceDTO(with: serialNumbers, maxDate: maxDate) } func latestVersionOfPass(req: Request) async throws -> Response { @@ -263,20 +295,20 @@ public final class PassKitCustom HTTPStatus { logger?.debug("Called unregisterDevice") - let type = req.parameters.get("type")! + let passTypeIdentifier = req.parameters.get("passTypeIdentifier")! guard let passId = req.parameters.get("passSerial", as: UUID.self) else { throw Abort(.badRequest) @@ -302,7 +334,7 @@ public final class PassKitCustom HTTPStatus { logger?.debug("Called logError") - let body: ErrorLogDto + let body: ErrorLogDTO do { - body = try req.content.decode(ErrorLogDto.self) + body = try req.content.decode(ErrorLogDTO.self) } catch { throw Abort(.badRequest) } @@ -339,9 +371,9 @@ public final class PassKitCustom HTTPStatus { - let r = try await R.for(deviceLibraryIdentifier: device.deviceLibraryIdentifier, passTypeIdentifier: pass.type, on: req.db) + let r = try await R.for(deviceLibraryIdentifier: device.deviceLibraryIdentifier, passTypeIdentifier: pass.passTypeIdentifier, on: req.db) .filter(P.self, \._$id == pass.id!) .first() if r != nil { @@ -376,12 +408,12 @@ public final class PassKitCustom, on db: any Database, app: Application) async throws { @@ -412,7 +444,7 @@ public final class PassKitCustom [R] { + private static func registrationsForPass(id: UUID, of passTypeIdentifier: 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. @@ -421,7 +453,7 @@ public final class PassKitCustom