diff --git a/.spi.yml b/.spi.yml
index bb99af5..ad36f6c 100644
--- a/.spi.yml
+++ b/.spi.yml
@@ -1,4 +1,4 @@
version: 1
builder:
configs:
- - documentation_targets: [Passes]
\ No newline at end of file
+ - documentation_targets: [PassKit, Passes, Orders]
\ No newline at end of file
diff --git a/Package.swift b/Package.swift
index 63291a0..ae85b76 100644
--- a/Package.swift
+++ b/Package.swift
@@ -7,29 +7,45 @@ let package = Package(
.macOS(.v13), .iOS(.v16)
],
products: [
- .library(name: "Passes", targets: ["Passes"]),
+ .library(name: "PassKit", targets: ["PassKit"]),
+ .library(name: "Passes", targets: ["PassKit", "Passes"]),
+ .library(name: "Orders", targets: ["PassKit", "Orders"]),
],
dependencies: [
.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.6.0"),
],
targets: [
.target(
- name: "Passes",
+ name: "PassKit",
dependencies: [
.product(name: "Fluent", package: "fluent"),
.product(name: "Vapor", package: "vapor"),
.product(name: "VaporAPNS", package: "apns"),
- .product(name: "Logging", package: "swift-log"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .target(
+ name: "Passes",
+ dependencies: [
+ .target(name: "PassKit"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .target(
+ name: "Orders",
+ dependencies: [
+ .target(name: "PassKit"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "PassKitTests",
dependencies: [
+ .target(name: "PassKit"),
.target(name: "Passes"),
+ .target(name: "Orders"),
.product(name: "XCTVapor", package: "vapor"),
],
swiftSettings: swiftSettings
diff --git a/README.md b/README.md
index 6a1b55b..8673607 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
![avatar](https://avatars.githubusercontent.com/u/26165732?s=200&v=4)
-
PassKit
+
PassKit
@@ -18,7 +18,7 @@
-๐๏ธ 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 Apple Wallet passes and orders.
### Major Releases
@@ -26,26 +26,26 @@ The table below shows a list of PassKit major releases alongside their compatibl
|Version|Swift|SPM|
|---|---|---|
-|0.3.0|5.10+|`from: "0.3.0"`|
+|0.4.0|5.10+|`from: "0.4.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")
+.package(url: "https://github.com/vapor-community/PassKit.git", from: "0.4.0")
```
-and add it to your target's dependencies:
+> Note: This package is made for Vapor 4.
+
+## ๐๏ธ Wallet Passes
+
+Add the `Passes` product to your target's dependencies:
```swift
.product(name: "Passes", package: "PassKit")
```
-> Note: This package requires Vapor 4.
-
-## Usage
-
### 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.
@@ -55,7 +55,7 @@ import Fluent
import struct Foundation.UUID
import Passes
-final class PassData: PassKitPassData, @unchecked Sendable {
+final class PassData: PassDataModel, @unchecked Sendable {
static let schema = "pass_data"
@ID
@@ -64,10 +64,15 @@ final class PassData: PassKitPassData, @unchecked Sendable {
@Parent(key: "pass_id")
var pass: PKPass
- // Add any other field relative to your app, such as a location, a date, etc.
+ // Examples of other extra fields:
@Field(key: "punches")
var punches: Int
+ @Field(key: "title")
+ var title: String
+
+ // Add any other field relative to your app, such as a location, a date, etc.
+
init() { }
}
@@ -75,8 +80,9 @@ 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, .references(PKPass.schema, .id, onDelete: .cascade))
+ .field("punches", .int, .required)
+ .field("title", .string, .required)
.create()
}
@@ -99,7 +105,7 @@ CREATE OR REPLACE FUNCTION public."RemoveUnregisteredItems"() RETURNS trigger
DELETE FROM devices d
WHERE NOT EXISTS (
SELECT 1
- FROM registrations r
+ FROM passes_registrations r
WHERE d."id" = r.device_id
LIMIT 1
);
@@ -107,7 +113,7 @@ CREATE OR REPLACE FUNCTION public."RemoveUnregisteredItems"() RETURNS trigger
DELETE FROM passes p
WHERE NOT EXISTS (
SELECT 1
- FROM registrations r
+ FROM passes_registrations r
WHERE p."id" = r.pass_id
LIMIT 1
);
@@ -117,49 +123,95 @@ END
$$;
CREATE TRIGGER "OnRegistrationDelete"
-AFTER DELETE ON "public"."registrations"
+AFTER DELETE ON "public"."passes_registrations"
FOR EACH ROW
EXECUTE PROCEDURE "public"."RemoveUnregisteredItems"();
```
+> [!CAUTION]
+> Be careful with SQL triggers, as they can have unintended consequences if not properly implemented.
+
### Model the `pass.json` contents
-Create a `struct` that implements `Encodable` which will contain all the fields for the generated `pass.json` file.
+Create a `struct` that implements `PassJSON` 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.
+
+> [!TIP]
+> 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.
+
+Here's an example of a `struct` that implements `PassJSON`.
```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]
- ...
+import Passes
- struct Barcode: Encodable {
- let altText: String
- let format = "PKBarcodeFormatQR"
+struct PassJSONData: PassJSON {
+ let description: String
+ let formatVersion = 1
+ let organizationName = "vapor-community"
+ let passTypeIdentifier = Environment.get("PASSKIT_PASS_TYPE_IDENTIFIER")!
+ let serialNumber: String
+ let teamIdentifier = Environment.get("APPLE_TEAM_IDENTIFIER")!
+
+ private let webServiceURL = "https://example.com/api/passes/"
+ private let authenticationToken: String
+ private let logoText = "Vapor"
+ private let sharingProhibited = true
+ let backgroundColor = "rgb(207, 77, 243)"
+ let foregroundColor = "rgb(255, 255, 255)"
+
+ let barcodes = Barcode(message: "test")
+ struct Barcode: Barcodes {
+ let format = BarcodeFormat.qr
let message: String
let messageEncoding = "iso-8859-1"
}
+ let boardingPass = Boarding(transitType: .air)
+ struct Boarding: BoardingPass {
+ let transitType: TransitType
+ let headerFields: [PassField]
+ let primaryFields: [PassField]
+ let secondaryFields: [PassField]
+ let auxiliaryFields: [PassField]
+ let backFields: [PassField]
+
+ struct PassField: PassFieldContent {
+ let key: String
+ let label: String
+ let value: String
+ }
+
+ init(transitType: TransitType) {
+ self.headerFields = [.init(key: "header", label: "Header", value: "Header")]
+ self.primaryFields = [.init(key: "primary", label: "Primary", value: "Primary")]
+ self.secondaryFields = [.init(key: "secondary", label: "Secondary", value: "Secondary")]
+ self.auxiliaryFields = [.init(key: "auxiliary", label: "Auxiliary", value: "Auxiliary")]
+ self.backFields = [.init(key: "back", label: "Back", value: "Back")]
+ self.transitType = transitType
+ }
+ }
+
init(data: PassData, pass: PKPass) {
- ...
+ self.description = data.title
+ self.serialNumber = pass.id!.uuidString
+ self.authenticationToken = pass.authenticationToken
}
}
```
+> [!IMPORTANT]
+> You **must** add `api/passes/` to your `webServiceURL`, as shown in the example above.
+
### Implement the delegate.
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).
+
+> [!TIP]
+> 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.
+
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
@@ -167,12 +219,12 @@ import Vapor
import Fluent
import Passes
-final class PKDelegate: PassesDelegate {
- let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/", isDirectory: true)
+final class PassDelegate: PassesDelegate {
+ let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Passes/", isDirectory: true)
let pemPrivateKeyPassword: String? = Environment.get("PEM_PRIVATE_KEY_PASSWORD")!
- func encode(pass: P, db: Database, encoder: JSONEncoder) async throws -> Data {
+ func encode(pass: P, db: Database, encoder: JSONEncoder) async throws -> Data {
// The specific PassData class you use here may vary based on the pass.type if you have multiple
// different types of passes, and thus multiple types of pass data.
guard let passData = try await PassData.query(on: db)
@@ -187,47 +239,57 @@ final class PKDelegate: PassesDelegate {
return data
}
- func template(for: P, db: Database) async throws -> URL {
+ func template(for: P, db: Database) async throws -> URL {
// The location might vary depending on the type of pass.
- return URL(fileURLWithPath: "PassKitTemplate/", isDirectory: true)
+ return URL(fileURLWithPath: "Templates/Passes/", isDirectory: true)
}
}
```
-You **must** explicitly declare `pemPrivateKeyPassword` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.
+> [!IMPORTANT]
+> You **must** explicitly declare `pemPrivateKeyPassword` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.
### 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!
+Next, register the routes in `routes.swift`.
This will implement all of the routes that PassKit expects to exist on your server for you.
```swift
import Vapor
import Passes
-let pkDelegate = PKDelegate()
+let passDelegate = PassDelegate()
func routes(_ app: Application) throws {
- let passes = Passes(app: app, delegate: pkDelegate)
- passes.registerRoutes(authorizationCode: PassJSONData.token)
+ let passesService = PassesService(app: app, delegate: passDelegate)
+ passesService.registerRoutes()
}
```
+> [!NOTE]
+> 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!
+
#### 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. If you don't include this line, you have to configure an APNS container yourself
+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.
+
+> [!IMPORTANT]
+> If you don't include this line, you have to configure an APNS container yourself
```swift
-try passes.registerPushRoutes(middleware: SecretMiddleware(secret: "foo"))
+try passesService.registerPushRoutes(middleware: SecretMiddleware(secret: "foo"))
```
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)
+- POST .../api/passes/v1/push/*:passTypeIdentifier*/*:passSerial* (Sends notifications)
+- GET .../api/passes/v1/push/*:passTypeIdentifier*/*:passSerial* (Retrieves a list of push tokens which would be sent a notification)
+
+#### Pass data model middleware
-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:
+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
@@ -243,7 +305,9 @@ struct PassDataMiddleware: AsyncModelMiddleware {
// 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")
+ let pkPass = PKPass(
+ passTypeIdentifier: "pass.com.yoursite.passType",
+ authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString())
try await pkPass.save(on: db)
model.$pass.id = try pkPass.requireID()
try await next.create(model, on: db)
@@ -254,7 +318,7 @@ struct PassDataMiddleware: AsyncModelMiddleware {
pkPass.updatedAt = Date()
try await pkPass.save(on: db)
try await next.update(model, on: db)
- try await Passes.sendPushNotifications(for: pkPass, on: db, app: self.app)
+ try await PassesService.sendPushNotifications(for: pkPass, on: db, app: self.app)
}
}
```
@@ -268,7 +332,9 @@ app.databases.middleware.use(PassDataMiddleware(app: app), on: .psql)
> [!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:
+#### APNSwift
+
+If you did not include the routes, remember to configure APNSwift yourself like this:
```swift
let apnsConfig: APNSClientConfiguration
@@ -302,12 +368,21 @@ app.apns.containers.use(
)
```
-#### Custom Implementation
+### Custom Implementation
+
+If you don't like the schema names that are used by default, you can instead create your own models conforming to `PassModel`, `DeviceModel`, `PassesRegistrationModel` and `ErrorLogModel` and instantiate the generic `PassesServiceCustom`, providing it 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
+import PassKit
+import Passes
+
+let passesService = PassesServiceCustom(app: app, delegate: delegate)
+```
+
+The `DeviceModel` and `ErrorLogModel` protocols are found inside the the `PassKit` product. If you want to customize the devices and error logs models you have to add it to the package manifest:
```swift
-let passes = PassesCustom(app: app, delegate: delegate)
+.product(name: "PassKit", package: "PassKit")
```
### Register Migrations
@@ -315,14 +390,15 @@ let passes = PassesCustom [!IMPORTANT]
+> Register the default models before the migration of your pass data model.
### Generate Pass Content
-To generate and distribute the `.pkpass` bundle, pass the `Passes` object to your `RouteCollection`:
+To generate and distribute the `.pkpass` bundle, pass the `PassesService` object to your `RouteCollection`:
```swift
import Fluent
@@ -330,7 +406,7 @@ import Vapor
import Passes
struct PassesController: RouteCollection {
- let passes: Passes
+ let passesService: PassesService
func boot(routes: RoutesBuilder) throws {
...
@@ -338,7 +414,7 @@ struct PassesController: RouteCollection {
}
```
-and then use it in the route handler:
+and then use it in route handlers:
```swift
fileprivate func passHandler(_ req: Request) async throws -> Response {
@@ -351,7 +427,7 @@ fileprivate func passHandler(_ req: Request) async throws -> Response {
throw Abort(.notFound)
}
- let bundle = try await passes.generatePassContent(for: passData.pass, on: req.db)
+ let bundle = try await passesService.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")
@@ -361,3 +437,15 @@ fileprivate func passHandler(_ req: Request) async throws -> Response {
return Response(status: .ok, headers: headers, body: body)
}
```
+
+## ๐ฆ Wallet Orders
+
+Add the `Orders` product to your target's dependencies:
+
+```swift
+.product(name: "Orders", package: "PassKit")
+```
+
+> [!WARNING]
+> The `Orders` is WIP, right now you can only set up the models and generate `.order` bundles.
+APNS support and order updates will be added soon. See the `Orders` target's documentation.
\ No newline at end of file
diff --git a/Sources/Orders/DTOs/OrdersForDeviceDTO.swift b/Sources/Orders/DTOs/OrdersForDeviceDTO.swift
new file mode 100644
index 0000000..921da6b
--- /dev/null
+++ b/Sources/Orders/DTOs/OrdersForDeviceDTO.swift
@@ -0,0 +1,18 @@
+//
+// OrdersForDeviceDTO.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import Vapor
+
+struct OrdersForDeviceDTO: Content {
+ let orderIdentifiers: [String]
+ let lastModified: String
+
+ init(with orderIdentifiers: [String], maxDate: Date) {
+ self.orderIdentifiers = orderIdentifiers
+ lastModified = String(maxDate.timeIntervalSince1970)
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/Middleware/AppleOrderMiddleware.swift b/Sources/Orders/Middleware/AppleOrderMiddleware.swift
new file mode 100644
index 0000000..517fe9d
--- /dev/null
+++ b/Sources/Orders/Middleware/AppleOrderMiddleware.swift
@@ -0,0 +1,22 @@
+//
+// AppleOrderMiddleware.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import Vapor
+import FluentKit
+
+struct AppleOrderMiddleware: AsyncMiddleware {
+ func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
+ guard let auth = request.headers["Authorization"].first?.replacingOccurrences(of: "AppleOrder ", with: ""),
+ let _ = try await O.query(on: request.db)
+ .filter(\._$authenticationToken == auth)
+ .first()
+ else {
+ throw Abort(.unauthorized)
+ }
+ return try await next.respond(to: request)
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/Models/Concrete Models/Order.swift b/Sources/Orders/Models/Concrete Models/Order.swift
new file mode 100644
index 0000000..d625205
--- /dev/null
+++ b/Sources/Orders/Models/Concrete Models/Order.swift
@@ -0,0 +1,57 @@
+//
+// Order.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import Foundation
+import FluentKit
+
+/// The `Model` that stores Wallet orders.
+open class Order: OrderModel, @unchecked Sendable {
+ public static let schema = Order.FieldKeys.schemaName
+
+ @ID
+ public var id: UUID?
+
+ @Timestamp(key: Order.FieldKeys.updatedAt, on: .update)
+ public var updatedAt: Date?
+
+ @Field(key: Order.FieldKeys.orderTypeIdentifier)
+ public var orderTypeIdentifier: String
+
+ @Field(key: Order.FieldKeys.authenticationToken)
+ public var authenticationToken: String
+
+ public required init() { }
+
+ public required init(orderTypeIdentifier: String, authenticationToken: String) {
+ self.orderTypeIdentifier = orderTypeIdentifier
+ self.authenticationToken = authenticationToken
+ }
+}
+
+extension Order: AsyncMigration {
+ public func prepare(on database: any Database) async throws {
+ try await database.schema(Self.schema)
+ .id()
+ .field(Order.FieldKeys.updatedAt, .datetime, .required)
+ .field(Order.FieldKeys.orderTypeIdentifier, .string, .required)
+ .field(Order.FieldKeys.authenticationToken, .string, .required)
+ .create()
+ }
+
+ public func revert(on database: any Database) async throws {
+ try await database.schema(Self.schema).delete()
+ }
+}
+
+extension Order {
+ enum FieldKeys {
+ static let schemaName = "orders"
+ static let updatedAt = FieldKey(stringLiteral: "updated_at")
+ static let orderTypeIdentifier = FieldKey(stringLiteral: "order_type_identifier")
+ static let authenticationToken = FieldKey(stringLiteral: "authentication_token")
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/Models/Concrete Models/OrdersDevice.swift b/Sources/Orders/Models/Concrete Models/OrdersDevice.swift
new file mode 100644
index 0000000..b2519ea
--- /dev/null
+++ b/Sources/Orders/Models/Concrete Models/OrdersDevice.swift
@@ -0,0 +1,53 @@
+//
+// OrdersDevice.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import FluentKit
+import PassKit
+
+/// The `Model` that stores Wallet orders devices.
+final public class OrdersDevice: DeviceModel, @unchecked Sendable {
+ public static let schema = OrdersDevice.FieldKeys.schemaName
+
+ @ID(custom: .id)
+ public var id: Int?
+
+ @Field(key: OrdersDevice.FieldKeys.pushToken)
+ public var pushToken: String
+
+ @Field(key: OrdersDevice.FieldKeys.deviceLibraryIdentifier)
+ public var deviceLibraryIdentifier: String
+
+ public init(deviceLibraryIdentifier: String, pushToken: String) {
+ self.deviceLibraryIdentifier = deviceLibraryIdentifier
+ self.pushToken = pushToken
+ }
+
+ public init() {}
+}
+
+extension OrdersDevice: AsyncMigration {
+ public func prepare(on database: any Database) async throws {
+ try await database.schema(Self.schema)
+ .field(.id, .int, .identifier(auto: true))
+ .field(OrdersDevice.FieldKeys.pushToken, .string, .required)
+ .field(OrdersDevice.FieldKeys.deviceLibraryIdentifier, .string, .required)
+ .unique(on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.deviceLibraryIdentifier)
+ .create()
+ }
+
+ public func revert(on database: any Database) async throws {
+ try await database.schema(Self.schema).delete()
+ }
+}
+
+extension OrdersDevice {
+ enum FieldKeys {
+ static let schemaName = "orders_devices"
+ static let pushToken = FieldKey(stringLiteral: "push_token")
+ static let deviceLibraryIdentifier = FieldKey(stringLiteral: "device_library_identifier")
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift b/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift
new file mode 100644
index 0000000..173e340
--- /dev/null
+++ b/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift
@@ -0,0 +1,52 @@
+//
+// OrdersErrorLog.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import struct Foundation.Date
+import FluentKit
+import PassKit
+
+/// The `Model` that stores Wallet orders error logs.
+final public class OrdersErrorLog: ErrorLogModel, @unchecked Sendable {
+ public static let schema = OrdersErrorLog.FieldKeys.schemaName
+
+ @ID(custom: .id)
+ public var id: Int?
+
+ @Timestamp(key: OrdersErrorLog.FieldKeys.createdAt, on: .create)
+ public var createdAt: Date?
+
+ @Field(key: OrdersErrorLog.FieldKeys.message)
+ public var message: String
+
+ public init(message: String) {
+ self.message = message
+ }
+
+ public init() {}
+}
+
+extension OrdersErrorLog: AsyncMigration {
+ public func prepare(on database: any Database) async throws {
+ try await database.schema(Self.schema)
+ .field(.id, .int, .identifier(auto: true))
+ .field(OrdersErrorLog.FieldKeys.createdAt, .datetime, .required)
+ .field(OrdersErrorLog.FieldKeys.message, .string, .required)
+ .create()
+ }
+
+ public func revert(on database: any Database) async throws {
+ try await database.schema(Self.schema).delete()
+ }
+}
+
+extension OrdersErrorLog {
+ enum FieldKeys {
+ static let schemaName = "orders_errors"
+ static let createdAt = FieldKey(stringLiteral: "created_at")
+ static let message = FieldKey(stringLiteral: "message")
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift b/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift
new file mode 100644
index 0000000..6acae38
--- /dev/null
+++ b/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift
@@ -0,0 +1,49 @@
+//
+// OrdersRegistration.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import FluentKit
+
+/// The `Model` that stores orders registrations.
+final public class OrdersRegistration: OrdersRegistrationModel, @unchecked Sendable {
+ public typealias OrderType = Order
+ public typealias DeviceType = OrdersDevice
+
+ public static let schema = OrdersRegistration.FieldKeys.schemaName
+
+ @ID(custom: .id)
+ public var id: Int?
+
+ @Parent(key: OrdersRegistration.FieldKeys.deviceID)
+ public var device: DeviceType
+
+ @Parent(key: OrdersRegistration.FieldKeys.orderID)
+ public var order: OrderType
+
+ public init() {}
+}
+
+extension OrdersRegistration: AsyncMigration {
+ public func prepare(on database: any Database) async throws {
+ try await database.schema(Self.schema)
+ .field(.id, .int, .identifier(auto: true))
+ .field(OrdersRegistration.FieldKeys.deviceID, .int, .required, .references(DeviceType.schema, .id, onDelete: .cascade))
+ .field(OrdersRegistration.FieldKeys.orderID, .uuid, .required, .references(OrderType.schema, .id, onDelete: .cascade))
+ .create()
+ }
+
+ public func revert(on database: any Database) async throws {
+ try await database.schema(Self.schema).delete()
+ }
+}
+
+extension OrdersRegistration {
+ enum FieldKeys {
+ static let schemaName = "orders_registrations"
+ static let deviceID = FieldKey(stringLiteral: "device_id")
+ static let orderID = FieldKey(stringLiteral: "order_id")
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/Models/OrderDataModel.swift b/Sources/Orders/Models/OrderDataModel.swift
new file mode 100644
index 0000000..55f3030
--- /dev/null
+++ b/Sources/Orders/Models/OrderDataModel.swift
@@ -0,0 +1,27 @@
+//
+// OrderDataModel.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import FluentKit
+
+/// Represents the `Model` that stores custom app data associated to Wallet orders.
+public protocol OrderDataModel: Model {
+ associatedtype OrderType: OrderModel
+
+ /// The foreign key to the order table
+ var order: OrderType { get set }
+}
+
+internal extension OrderDataModel {
+ var _$order: Parent {
+ guard let mirror = Mirror(reflecting: self).descendant("_order"),
+ let order = mirror as? Parent else {
+ fatalError("order property must be declared using @Parent")
+ }
+
+ return order
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/Models/OrderModel.swift b/Sources/Orders/Models/OrderModel.swift
new file mode 100644
index 0000000..09f3221
--- /dev/null
+++ b/Sources/Orders/Models/OrderModel.swift
@@ -0,0 +1,61 @@
+//
+// OrderModel.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import Foundation
+import FluentKit
+
+/// Represents the `Model` that stores Waller orders.
+///
+/// Uses a UUID so people can't easily guess order IDs
+public protocol OrderModel: Model where IDValue == UUID {
+ /// The order type identifier.
+ var orderTypeIdentifier: String { get set }
+
+ /// The last time the order was modified.
+ var updatedAt: Date? { get set }
+
+ /// The authentication token for the order.
+ var authenticationToken: String { get set }
+}
+
+internal extension OrderModel {
+ var _$id: ID {
+ guard let mirror = Mirror(reflecting: self).descendant("_id"),
+ let id = mirror as? ID else {
+ fatalError("id property must be declared using @ID")
+ }
+
+ return id
+ }
+
+ var _$orderTypeIdentifier: Field {
+ guard let mirror = Mirror(reflecting: self).descendant("_orderTypeIdentifier"),
+ let orderTypeIdentifier = mirror as? Field else {
+ fatalError("orderTypeIdentifier property must be declared using @Field")
+ }
+
+ return orderTypeIdentifier
+ }
+
+ 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 updatedAt
+ }
+
+ var _$authenticationToken: Field {
+ guard let mirror = Mirror(reflecting: self).descendant("_authenticationToken"),
+ let authenticationToken = mirror as? Field else {
+ fatalError("authenticationToken property must be declared using @Field")
+ }
+
+ return authenticationToken
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/Models/OrdersRegistrationModel.swift b/Sources/Orders/Models/OrdersRegistrationModel.swift
new file mode 100644
index 0000000..ffcc912
--- /dev/null
+++ b/Sources/Orders/Models/OrdersRegistrationModel.swift
@@ -0,0 +1,51 @@
+//
+// OrdersRegistrationModel.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+import FluentKit
+import PassKit
+
+/// Represents the `Model` that stores orders registrations.
+public protocol OrdersRegistrationModel: Model where IDValue == Int {
+ associatedtype OrderType: OrderModel
+ associatedtype DeviceType: DeviceModel
+
+ /// The device for this registration.
+ var device: DeviceType { get set }
+
+ /// The order for this registration.
+ var order: OrderType { get set }
+}
+
+internal extension OrdersRegistrationModel {
+ var _$device: Parent {
+ guard let mirror = Mirror(reflecting: self).descendant("_device"),
+ let device = mirror as? Parent else {
+ fatalError("device property must be declared using @Parent")
+ }
+
+ return device
+ }
+
+ var _$order: Parent {
+ guard let mirror = Mirror(reflecting: self).descendant("_order"),
+ let order = mirror as? Parent else {
+ fatalError("order property must be declared using @Parent")
+ }
+
+ return order
+ }
+
+ static func `for`(deviceLibraryIdentifier: String, orderTypeIdentifier: String, on db: any Database) -> QueryBuilder {
+ Self.query(on: db)
+ .join(parent: \._$order)
+ .join(parent: \._$device)
+ .with(\._$order)
+ .with(\._$device)
+ .filter(OrderType.self, \._$orderTypeIdentifier == orderTypeIdentifier)
+ .filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier)
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/OrdersDelegate.swift b/Sources/Orders/OrdersDelegate.swift
new file mode 100644
index 0000000..87a784f
--- /dev/null
+++ b/Sources/Orders/OrdersDelegate.swift
@@ -0,0 +1,120 @@
+//
+// OrdersDelegate.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 01/07/24.
+//
+
+import Foundation
+import FluentKit
+
+/// The delegate which is responsible for generating the order files.
+public protocol OrdersDelegate: AnyObject, Sendable {
+ /// Should return a `URL` which points to the template data for the order.
+ ///
+ /// The URL should point to a directory containing all the images and localizations for the generated `.order` archive but should *not* contain any of these items:
+ /// - `manifest.json`
+ /// - `order.json`
+ /// - `signature`
+ ///
+ /// - Parameters:
+ /// - for: The order data from the SQL server.
+ /// - db: The SQL database to query against.
+ ///
+ /// - Returns: A `URL` which points to the template data for the order.
+ ///
+ /// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor.
+ func template(for: O, db: any Database) async throws -> URL
+
+ /// Generates the SSL `signature` file.
+ ///
+ /// If you need to implement custom S/Mime signing you can use this
+ /// method to do so. You must generate a detached DER signature of the `manifest.json` file.
+ ///
+ /// - Parameter root: The location of the `manifest.json` and where to write the `signature` to.
+ /// - Returns: Return `true` if you generated a custom `signature`, otherwise `false`.
+ func generateSignatureFile(in root: URL) -> Bool
+
+ /// Encode the order into JSON.
+ ///
+ /// This method should generate the entire order JSON. You are provided with
+ /// the order data from the SQL database and you should return a properly
+ /// formatted order file encoding.
+ ///
+ /// - Parameters:
+ /// - order: The order data from the SQL server
+ /// - db: The SQL database to query against.
+ /// - encoder: The `JSONEncoder` which you should use.
+ /// - Returns: The encoded order JSON data.
+ ///
+ /// > Tip: See the [`Order`](https://developer.apple.com/documentation/walletorders/order) object to understand the keys.
+ func encode(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data
+
+ /// Should return a `URL` which points to the template data for the order.
+ ///
+ /// The URL should point to a directory containing the files specified by these keys:
+ /// - `wwdrCertificate`
+ /// - `pemCertificate`
+ /// - `pemPrivateKey`
+ ///
+ /// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` initializer!
+ var sslSigningFilesDirectory: URL { get }
+
+ /// The location of the `openssl` command as a file URL.
+ ///
+ /// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor.
+ var sslBinary: URL { get }
+
+ /// The full path to the `zip` command as a file URL.
+ ///
+ /// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor.
+ var zipBinary: URL { get }
+
+ /// The name of Apple's WWDR.pem certificate as contained in `sslSigningFiles` path.
+ ///
+ /// Defaults to `WWDR.pem`
+ var wwdrCertificate: String { get }
+
+ /// The name of the PEM Certificate for signing the order as contained in `sslSigningFiles` path.
+ ///
+ /// Defaults to `ordercertificate.pem`
+ var pemCertificate: String { get }
+
+ /// The name of the PEM Certificate's private key for signing the order as contained in `sslSigningFiles` path.
+ ///
+ /// Defaults to `orderkey.pem`
+ var pemPrivateKey: String { get }
+
+ /// The password to the private key file.
+ var pemPrivateKeyPassword: String? { get }
+}
+
+public extension OrdersDelegate {
+ var wwdrCertificate: String {
+ get { return "WWDR.pem" }
+ }
+
+ var pemCertificate: String {
+ get { return "ordercertificate.pem" }
+ }
+
+ var pemPrivateKey: String {
+ get { return "orderkey.pem" }
+ }
+
+ var pemPrivateKeyPassword: String? {
+ get { return nil }
+ }
+
+ var sslBinary: URL {
+ get { return URL(fileURLWithPath: "/usr/bin/openssl") }
+ }
+
+ var zipBinary: URL {
+ get { return URL(fileURLWithPath: "/usr/bin/zip") }
+ }
+
+ func generateSignatureFile(in root: URL) -> Bool {
+ return false
+ }
+}
\ No newline at end of file
diff --git a/Sources/Orders/OrdersError.swift b/Sources/Orders/OrdersError.swift
new file mode 100644
index 0000000..223dc75
--- /dev/null
+++ b/Sources/Orders/OrdersError.swift
@@ -0,0 +1,26 @@
+//
+// OrdersError.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 30/06/24.
+//
+
+public enum OrdersError: Error {
+ /// The template path is not a directory
+ case templateNotDirectory
+
+ /// The `pemCertificate` file is missing.
+ case pemCertificateMissing
+
+ /// The `pemPrivateKey` file is missing.
+ case pemPrivateKeyMissing
+
+ /// Swift NIO failed to read the key.
+ case nioPrivateKeyReadFailed(any Error)
+
+ /// The path to the zip binary is incorrect.
+ case zipBinaryMissing
+
+ /// The path to the openssl binary is incorrect
+ case opensslBinaryMissing
+}
\ No newline at end of file
diff --git a/Sources/Orders/OrdersService.swift b/Sources/Orders/OrdersService.swift
new file mode 100644
index 0000000..23977ba
--- /dev/null
+++ b/Sources/Orders/OrdersService.swift
@@ -0,0 +1,38 @@
+//
+// OrdersService.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 01/07/24.
+//
+
+import Vapor
+import FluentKit
+
+/// The main class that handles Wallet orders.
+public final class OrdersService: Sendable {
+ private let service: OrdersServiceCustom
+
+ public init(app: Application, delegate: any OrdersDelegate, logger: Logger? = nil) {
+ service = .init(app: app, delegate: delegate, logger: logger)
+ }
+
+ /// Generates the order content bundle for a given order.
+ ///
+ /// - Parameters:
+ /// - order: The order to generate the content for.
+ /// - db: The `Database` to use.
+ /// - Returns: The generated order content.
+ public func generateOrderContent(for order: Order, on db: any Database) async throws -> Data {
+ try await service.generateOrderContent(for: order, on: db)
+ }
+
+ /// Adds the migrations for Wallet orders models.
+ ///
+ /// - Parameter migrations: The `Migrations` object to add the migrations to.
+ public static func register(migrations: Migrations) {
+ migrations.add(Order())
+ migrations.add(OrdersDevice())
+ migrations.add(OrdersRegistration())
+ migrations.add(OrdersErrorLog())
+ }
+}
diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift
new file mode 100644
index 0000000..c70b974
--- /dev/null
+++ b/Sources/Orders/OrdersServiceCustom.swift
@@ -0,0 +1,147 @@
+//
+// OrdersServiceCustom.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 01/07/24.
+//
+
+@preconcurrency import Vapor
+import APNS
+import VaporAPNS
+@preconcurrency import APNSCore
+import Fluent
+import NIOSSL
+import PassKit
+
+/// Class to handle `OrdersService`.
+///
+/// The generics should be passed in this order:
+/// - Order Type
+/// - Device Type
+/// - Registration Type
+/// - Error Log Type
+public final class OrdersServiceCustom: Sendable where O == R.OrderType, D == R.DeviceType {
+ public unowned let delegate: any OrdersDelegate
+ private unowned let app: Application
+
+ private let v1: any RoutesBuilder
+ private let logger: Logger?
+
+ public init(app: Application, delegate: any OrdersDelegate, logger: Logger? = nil) {
+ self.delegate = delegate
+ self.logger = logger
+ self.app = app
+
+ v1 = app.grouped("api", "orders", "v1")
+ }
+}
+
+// MARK: - order file generation
+extension OrdersServiceCustom {
+ private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws {
+ var manifest: [String: String] = [:]
+
+ let paths = try FileManager.default.subpathsOfDirectory(atPath: root.unixPath())
+ try paths.forEach { relativePath in
+ let file = URL(fileURLWithPath: relativePath, relativeTo: root)
+ guard !file.hasDirectoryPath else {
+ return
+ }
+
+ let data = try Data(contentsOf: file)
+ let hash = SHA256.hash(data: data)
+ manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined()
+ }
+
+ let encoded = try encoder.encode(manifest)
+ try encoded.write(to: root.appendingPathComponent("manifest.json"))
+ }
+
+ private func generateSignatureFile(in root: URL) throws {
+ if delegate.generateSignatureFile(in: root) {
+ // If the caller's delegate generated a file we don't have to do it.
+ return
+ }
+
+ let sslBinary = delegate.sslBinary
+
+ guard FileManager.default.fileExists(atPath: sslBinary.unixPath()) else {
+ throw OrdersError.opensslBinaryMissing
+ }
+
+ let proc = Process()
+ proc.currentDirectoryURL = delegate.sslSigningFilesDirectory
+ proc.executableURL = sslBinary
+
+ proc.arguments = [
+ "smime", "-binary", "-sign",
+ "-certfile", delegate.wwdrCertificate,
+ "-signer", delegate.pemCertificate,
+ "-inkey", delegate.pemPrivateKey,
+ "-in", root.appendingPathComponent("manifest.json").unixPath(),
+ "-out", root.appendingPathComponent("signature").unixPath(),
+ "-outform", "DER"
+ ]
+
+ if let pwd = delegate.pemPrivateKeyPassword {
+ proc.arguments!.append(contentsOf: ["-passin", "pass:\(pwd)"])
+ }
+
+ try proc.run()
+
+ proc.waitUntilExit()
+ }
+
+ private func zip(directory: URL, to: URL) throws {
+ let zipBinary = delegate.zipBinary
+ guard FileManager.default.fileExists(atPath: zipBinary.unixPath()) else {
+ throw OrdersError.zipBinaryMissing
+ }
+
+ let proc = Process()
+ proc.currentDirectoryURL = directory
+ proc.executableURL = zipBinary
+
+ proc.arguments = [ to.unixPath(), "-r", "-q", "." ]
+
+ try proc.run()
+ proc.waitUntilExit()
+ }
+
+ public func generateOrderContent(for order: O, on db: any Database) async throws -> Data {
+ let tmp = FileManager.default.temporaryDirectory
+ let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true)
+ let zipFile = tmp.appendingPathComponent("\(UUID().uuidString).zip")
+ let encoder = JSONEncoder()
+
+ let src = try await delegate.template(for: order, db: db)
+ guard (try? src.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else {
+ throw OrdersError.templateNotDirectory
+ }
+
+ let encoded = try await self.delegate.encode(order: order, db: db, encoder: encoder)
+
+ do {
+ try FileManager.default.copyItem(at: src, to: root)
+
+ defer {
+ _ = try? FileManager.default.removeItem(at: root)
+ }
+
+ try encoded.write(to: root.appendingPathComponent("order.json"))
+
+ try Self.generateManifestFile(using: encoder, in: root)
+ try self.generateSignatureFile(in: root)
+
+ try self.zip(directory: root, to: zipFile)
+
+ defer {
+ _ = try? FileManager.default.removeItem(at: zipFile)
+ }
+
+ return try Data(contentsOf: zipFile)
+ } catch {
+ throw error
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sources/Passes/DTOs/ErrorLogDTO.swift b/Sources/PassKit/DTOs/ErrorLogDTO.swift
similarity index 96%
rename from Sources/Passes/DTOs/ErrorLogDTO.swift
rename to Sources/PassKit/DTOs/ErrorLogDTO.swift
index a1af6f7..3096031 100644
--- a/Sources/Passes/DTOs/ErrorLogDTO.swift
+++ b/Sources/PassKit/DTOs/ErrorLogDTO.swift
@@ -28,6 +28,6 @@
import Vapor
-struct ErrorLogDTO: Content {
- let logs: [String]
+package struct ErrorLogDTO: Content {
+ package let logs: [String]
}
diff --git a/Sources/Passes/DTOs/RegistrationDTO.swift b/Sources/PassKit/DTOs/RegistrationDTO.swift
similarity index 95%
rename from Sources/Passes/DTOs/RegistrationDTO.swift
rename to Sources/PassKit/DTOs/RegistrationDTO.swift
index a0a2068..179ec41 100644
--- a/Sources/Passes/DTOs/RegistrationDTO.swift
+++ b/Sources/PassKit/DTOs/RegistrationDTO.swift
@@ -28,6 +28,6 @@
import Vapor
-struct RegistrationDTO: Content {
- let pushToken: String
+package struct RegistrationDTO: Content {
+ package let pushToken: String
}
diff --git a/Sources/Passes/Models/PassKitDevice.swift b/Sources/PassKit/Models/DeviceModel.swift
similarity index 96%
rename from Sources/Passes/Models/PassKitDevice.swift
rename to Sources/PassKit/Models/DeviceModel.swift
index 2df7b68..50f86e1 100644
--- a/Sources/Passes/Models/PassKitDevice.swift
+++ b/Sources/PassKit/Models/DeviceModel.swift
@@ -26,11 +26,10 @@
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
-import Vapor
-import Fluent
+import FluentKit
/// Represents the `Model` that stores PassKit devices.
-public protocol PassKitDevice: Model where IDValue == Int {
+public protocol DeviceModel: Model where IDValue == Int {
/// The push token used for sending updates to the device.
var pushToken: String { get set }
@@ -44,7 +43,7 @@ public protocol PassKitDevice: Model where IDValue == Int {
init(deviceLibraryIdentifier: String, pushToken: String)
}
-internal extension PassKitDevice {
+package extension DeviceModel {
var _$id: ID {
guard let mirror = Mirror(reflecting: self).descendant("_id"),
let id = mirror as? ID else {
diff --git a/Sources/Passes/Models/PassKitErrorLog.swift b/Sources/PassKit/Models/ErrorLogModel.swift
similarity index 95%
rename from Sources/Passes/Models/PassKitErrorLog.swift
rename to Sources/PassKit/Models/ErrorLogModel.swift
index b98ddc6..fc417df 100644
--- a/Sources/Passes/Models/PassKitErrorLog.swift
+++ b/Sources/PassKit/Models/ErrorLogModel.swift
@@ -26,11 +26,10 @@
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
-import Vapor
-import Fluent
+import FluentKit
/// Represents the `Model` that stores PassKit error logs.
-public protocol PassKitErrorLog: Model {
+public protocol ErrorLogModel: Model {
/// The error message provided by PassKit
var message: String { get set }
@@ -39,7 +38,7 @@ public protocol PassKitErrorLog: Model {
init(message: String)
}
-internal extension PassKitErrorLog {
+package extension ErrorLogModel {
var _$message: Field {
guard let mirror = Mirror(reflecting: self).descendant("_message"),
let message = mirror as? Field else {
diff --git a/Sources/Passes/URL+Extension.swift b/Sources/PassKit/URL+Extension.swift
similarity index 97%
rename from Sources/Passes/URL+Extension.swift
rename to Sources/PassKit/URL+Extension.swift
index 5803298..5114164 100644
--- a/Sources/Passes/URL+Extension.swift
+++ b/Sources/PassKit/URL+Extension.swift
@@ -29,7 +29,7 @@
import Foundation
extension URL {
- func unixPath() -> String {
+ package func unixPath() -> String {
absoluteString.replacingOccurrences(of: "file://", with: "")
}
}
diff --git a/Sources/Passes/DTOs/PassJSON.swift b/Sources/Passes/DTOs/PassJSON.swift
new file mode 100644
index 0000000..0cccc7e
--- /dev/null
+++ b/Sources/Passes/DTOs/PassJSON.swift
@@ -0,0 +1,93 @@
+//
+// PassJSON.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 28/06/24.
+//
+
+/// A protocol that defines the structure of a `pass.json` file.
+///
+/// > Tip: See the [`Pass`](https://developer.apple.com/documentation/walletpasses/pass) object to understand the keys.
+public protocol PassJSON: Encodable {
+ /// A short description that iOS accessibility technologies use for a pass.
+ var description: String { get }
+
+ /// The version of the file format. The value must be 1.
+ var formatVersion: Int { get }
+
+ /// The name of the organization.
+ var organizationName: String { get }
+
+ /// The pass type identifier thatโs registered with Apple.
+ ///
+ /// The value must be the same as the distribution certificate used to sign the pass.
+ var passTypeIdentifier: String { get }
+
+ /// An alphanumeric serial number.
+ ///
+ /// The combination of the serial number and pass type identifier must be unique for each pass.
+ var serialNumber: String { get }
+
+ /// The Team ID for the Apple Developer Program account that registered the pass type identifier.
+ var teamIdentifier: String { get }
+}
+
+/// A protocol that represents the information to display in a field on a pass.
+///
+/// > Tip: See the [`PassFieldContent`](https://developer.apple.com/documentation/walletpasses/passfieldcontent) object to understand the keys.
+public protocol PassFieldContent: Encodable {
+ /// A unique key that identifies a field in the pass; for example, `departure-gate`.
+ var key: String { get }
+
+ /// The value to use for the field; for example, 42.
+ ///
+ /// A date or time value must include a time zone.
+ var value: String { get }
+}
+
+/// A protocol that represents the groups of fields that display the information for a boarding pass.
+///
+/// > Tip: See the [`Pass.BoardingPass`](https://developer.apple.com/documentation/walletpasses/pass/boardingpass) object to understand the keys.
+public protocol BoardingPass: Encodable {
+ /// The type of transit for a boarding pass.
+ ///
+ /// This key is invalid for other types of passes.
+ ///
+ /// The system may use the value to display more information,
+ /// such as showing an airplane icon for the pass on watchOS when the value is set to `PKTransitTypeAir`.
+ var transitType: TransitType { get }
+}
+
+/// The type of transit for a boarding pass.
+public enum TransitType: String, Encodable {
+ case air = "PKTransitTypeAir"
+ case boat = "PKTransitTypeBoat"
+ case bus = "PKTransitTypeBus"
+ case generic = "PKTransitTypeGeneric"
+ case train = "PKTransitTypeTrain"
+}
+
+/// A protocol that represents a barcode on a pass.
+///
+/// > Tip: See the [`Pass.Barcodes`](https://developer.apple.com/documentation/walletpasses/pass/barcodes) object to understand the keys.
+public protocol Barcodes: Encodable {
+ /// The format of the barcode.
+ ///
+ /// The barcode format `PKBarcodeFormatCode128` isnโt supported for watchOS.
+ var format: BarcodeFormat { get }
+
+ /// The message or payload to display as a barcode.
+ var message: String { get }
+
+ /// The IANA character set name of the text encoding to use to convert message
+ /// from a string representation to a data representation that the system renders as a barcode, such as `iso-8859-1`.
+ var messageEncoding: String { get }
+}
+
+/// The format of the barcode.
+public enum BarcodeFormat: String, Encodable {
+ case pdf417 = "PKBarcodeFormatPDF417"
+ case qr = "PKBarcodeFormatQR"
+ case aztec = "PKBarcodeFormatAztec"
+ case code128 = "PKBarcodeFormatCode128"
+}
diff --git a/Sources/Passes/FakeSendable.swift b/Sources/Passes/FakeSendable.swift
deleted file mode 100644
index b048685..0000000
--- a/Sources/Passes/FakeSendable.swift
+++ /dev/null
@@ -1,32 +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.
-
-// This is a temporary fix until RoutesBuilder and EmptyPayload are not Sendable
-struct FakeSendable: @unchecked Sendable {
- let value: T
-}
diff --git a/Sources/Passes/Middleware/ApplePassMiddleware.swift b/Sources/Passes/Middleware/ApplePassMiddleware.swift
index cd06789..ee498e5 100644
--- a/Sources/Passes/Middleware/ApplePassMiddleware.swift
+++ b/Sources/Passes/Middleware/ApplePassMiddleware.swift
@@ -27,13 +27,15 @@
/// THE SOFTWARE.
import Vapor
+import FluentKit
-struct ApplePassMiddleware: AsyncMiddleware {
- let authorizationCode: String
-
+struct ApplePassMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
- let auth = request.headers["Authorization"]
- guard auth.first == "ApplePass \(authorizationCode)" else {
+ guard let auth = request.headers["Authorization"].first?.replacingOccurrences(of: "ApplePass ", with: ""),
+ let _ = try await P.query(on: request.db)
+ .filter(\._$authenticationToken == auth)
+ .first()
+ else {
throw Abort(.unauthorized)
}
return try await next.respond(to: request)
diff --git a/Sources/Passes/Models/Concrete Models/PKPass.swift b/Sources/Passes/Models/Concrete Models/PKPass.swift
new file mode 100644
index 0000000..4103a09
--- /dev/null
+++ b/Sources/Passes/Models/Concrete Models/PKPass.swift
@@ -0,0 +1,57 @@
+//
+// PKPass.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 29/06/24.
+//
+
+import Foundation
+import FluentKit
+
+/// The `Model` that stores PassKit passes.
+open class PKPass: PassModel, @unchecked Sendable {
+ public static let schema = PKPass.FieldKeys.schemaName
+
+ @ID
+ public var id: UUID?
+
+ @Timestamp(key: PKPass.FieldKeys.updatedAt, on: .update)
+ public var updatedAt: Date?
+
+ @Field(key: PKPass.FieldKeys.passTypeIdentifier)
+ public var passTypeIdentifier: String
+
+ @Field(key: PKPass.FieldKeys.authenticationToken)
+ public var authenticationToken: String
+
+ public required init() { }
+
+ public required init(passTypeIdentifier: String, authenticationToken: String) {
+ self.passTypeIdentifier = passTypeIdentifier
+ self.authenticationToken = authenticationToken
+ }
+}
+
+extension PKPass: AsyncMigration {
+ public func prepare(on database: any Database) async throws {
+ try await database.schema(Self.schema)
+ .id()
+ .field(PKPass.FieldKeys.updatedAt, .datetime, .required)
+ .field(PKPass.FieldKeys.passTypeIdentifier, .string, .required)
+ .field(PKPass.FieldKeys.authenticationToken, .string, .required)
+ .create()
+ }
+
+ public func revert(on database: any Database) async throws {
+ try await database.schema(Self.schema).delete()
+ }
+}
+
+extension PKPass {
+ enum FieldKeys {
+ static let schemaName = "passes"
+ static let updatedAt = FieldKey(stringLiteral: "updated_at")
+ static let passTypeIdentifier = FieldKey(stringLiteral: "pass_type_identifier")
+ static let authenticationToken = FieldKey(stringLiteral: "authentication_token")
+ }
+}
diff --git a/Sources/Passes/Models/Concrete Models/PassesDevice.swift b/Sources/Passes/Models/Concrete Models/PassesDevice.swift
new file mode 100644
index 0000000..c06eba0
--- /dev/null
+++ b/Sources/Passes/Models/Concrete Models/PassesDevice.swift
@@ -0,0 +1,53 @@
+//
+// PassesDevice.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 29/06/24.
+//
+
+import FluentKit
+import PassKit
+
+/// The `Model` that stores PassKit passes devices.
+final public class PassesDevice: DeviceModel, @unchecked Sendable {
+ public static let schema = PassesDevice.FieldKeys.schemaName
+
+ @ID(custom: .id)
+ public var id: Int?
+
+ @Field(key: PassesDevice.FieldKeys.pushToken)
+ public var pushToken: String
+
+ @Field(key: PassesDevice.FieldKeys.deviceLibraryIdentifier)
+ public var deviceLibraryIdentifier: String
+
+ public init(deviceLibraryIdentifier: String, pushToken: String) {
+ self.deviceLibraryIdentifier = deviceLibraryIdentifier
+ self.pushToken = pushToken
+ }
+
+ public init() {}
+}
+
+extension PassesDevice: AsyncMigration {
+ public func prepare(on database: any Database) async throws {
+ try await database.schema(Self.schema)
+ .field(.id, .int, .identifier(auto: true))
+ .field(PassesDevice.FieldKeys.pushToken, .string, .required)
+ .field(PassesDevice.FieldKeys.deviceLibraryIdentifier, .string, .required)
+ .unique(on: PassesDevice.FieldKeys.pushToken, PassesDevice.FieldKeys.deviceLibraryIdentifier)
+ .create()
+ }
+
+ public func revert(on database: any Database) async throws {
+ try await database.schema(Self.schema).delete()
+ }
+}
+
+extension PassesDevice {
+ enum FieldKeys {
+ static let schemaName = "passes_devices"
+ static let pushToken = FieldKey(stringLiteral: "push_token")
+ static let deviceLibraryIdentifier = FieldKey(stringLiteral: "device_library_identifier")
+ }
+}
diff --git a/Sources/Passes/Models/Concrete Models/PassesErrorLog.swift b/Sources/Passes/Models/Concrete Models/PassesErrorLog.swift
new file mode 100644
index 0000000..3e0497c
--- /dev/null
+++ b/Sources/Passes/Models/Concrete Models/PassesErrorLog.swift
@@ -0,0 +1,52 @@
+//
+// PassesErrorLog.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 29/06/24.
+//
+
+import struct Foundation.Date
+import FluentKit
+import PassKit
+
+/// The `Model` that stores PassKit passes error logs.
+final public class PassesErrorLog: ErrorLogModel, @unchecked Sendable {
+ public static let schema = PassesErrorLog.FieldKeys.schemaName
+
+ @ID(custom: .id)
+ public var id: Int?
+
+ @Timestamp(key: PassesErrorLog.FieldKeys.createdAt, on: .create)
+ public var createdAt: Date?
+
+ @Field(key: PassesErrorLog.FieldKeys.message)
+ public var message: String
+
+ public init(message: String) {
+ self.message = message
+ }
+
+ public init() {}
+}
+
+extension PassesErrorLog: AsyncMigration {
+ public func prepare(on database: any Database) async throws {
+ try await database.schema(Self.schema)
+ .field(.id, .int, .identifier(auto: true))
+ .field(PassesErrorLog.FieldKeys.createdAt, .datetime, .required)
+ .field(PassesErrorLog.FieldKeys.message, .string, .required)
+ .create()
+ }
+
+ public func revert(on database: any Database) async throws {
+ try await database.schema(Self.schema).delete()
+ }
+}
+
+extension PassesErrorLog {
+ enum FieldKeys {
+ static let schemaName = "passes_errors"
+ static let createdAt = FieldKey(stringLiteral: "created_at")
+ static let message = FieldKey(stringLiteral: "message")
+ }
+}
diff --git a/Sources/Passes/Models/Concrete Models/PassesRegistration.swift b/Sources/Passes/Models/Concrete Models/PassesRegistration.swift
new file mode 100644
index 0000000..35cea46
--- /dev/null
+++ b/Sources/Passes/Models/Concrete Models/PassesRegistration.swift
@@ -0,0 +1,49 @@
+//
+// PassesRegistration.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 29/06/24.
+//
+
+import FluentKit
+
+/// The `Model` that stores passes registrations.
+final public class PassesRegistration: PassesRegistrationModel, @unchecked Sendable {
+ public typealias PassType = PKPass
+ public typealias DeviceType = PassesDevice
+
+ public static let schema = PassesRegistration.FieldKeys.schemaName
+
+ @ID(custom: .id)
+ public var id: Int?
+
+ @Parent(key: PassesRegistration.FieldKeys.deviceID)
+ public var device: DeviceType
+
+ @Parent(key: PassesRegistration.FieldKeys.passID)
+ public var pass: PassType
+
+ public init() {}
+}
+
+extension PassesRegistration: AsyncMigration {
+ public func prepare(on database: any Database) async throws {
+ try await database.schema(Self.schema)
+ .field(.id, .int, .identifier(auto: true))
+ .field(PassesRegistration.FieldKeys.deviceID, .int, .required, .references(DeviceType.schema, .id, onDelete: .cascade))
+ .field(PassesRegistration.FieldKeys.passID, .uuid, .required, .references(PassType.schema, .id, onDelete: .cascade))
+ .create()
+ }
+
+ public func revert(on database: any Database) async throws {
+ try await database.schema(Self.schema).delete()
+ }
+}
+
+extension PassesRegistration {
+ enum FieldKeys {
+ static let schemaName = "passes_registrations"
+ static let deviceID = FieldKey(stringLiteral: "device_id")
+ static let passID = FieldKey(stringLiteral: "pass_id")
+ }
+}
diff --git a/Sources/Passes/Models/ConcreteModels.swift b/Sources/Passes/Models/ConcreteModels.swift
deleted file mode 100644
index f6c7bea..0000000
--- a/Sources/Passes/Models/ConcreteModels.swift
+++ /dev/null
@@ -1,169 +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 Fluent
-
-/// The `Model` that stores PassKit devices.
-final public class PKDevice: PassKitDevice, @unchecked Sendable {
- public static let schema = "devices"
-
- @ID(custom: .id)
- public var id: Int?
-
- @Field(key: "push_token")
- public var pushToken: String
-
- @Field(key: "device_library_identifier")
- public var deviceLibraryIdentifier: String
-
- public init(deviceLibraryIdentifier: String, pushToken: String) {
- self.deviceLibraryIdentifier = deviceLibraryIdentifier
- self.pushToken = pushToken
- }
-
- public init() {}
-}
-
-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: any Database) async throws {
- try await database.schema(Self.schema).delete()
- }
-}
-
-/// The `Model` that stores PassKit passes.
-open class PKPass: PassKitPass, @unchecked Sendable {
- public static let schema = "passes"
-
- @ID
- public var id: UUID?
-
- @Timestamp(key: "updated_at", on: .update)
- public var updatedAt: Date?
-
- @Field(key: "pass_type_identifier")
- public var passTypeIdentifier: String
-
- public required init() { }
-
- public required init(passTypeIdentifier: String) {
- self.passTypeIdentifier = passTypeIdentifier
- }
-}
-
-extension PKPass: AsyncMigration {
- public func prepare(on database: any Database) async throws {
- try await database.schema(Self.schema)
- .id()
- .field("updated_at", .datetime, .required)
- .field("pass_type_identifier", .string, .required)
- .create()
- }
-
- public func revert(on database: any Database) async throws {
- try await database.schema(Self.schema).delete()
- }
-}
-
-/// 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?
-
- @Timestamp(key: "created_at", on: .create)
- public var createdAt: Date?
-
- @Field(key: "message")
- public var message: String
-
- public init(message: String) {
- self.message = message
- }
-
- public init() {}
-}
-
-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: any Database) async throws {
- try await database.schema(PKErrorLog.schema).delete()
- }
-}
-
-/// The `Model` that stores PassKit registrations.
-final public class PKRegistration: PassKitRegistration, @unchecked Sendable {
- public typealias PassType = PKPass
- public typealias DeviceType = PKDevice
-
- public static let schema = "registrations"
-
- @ID(custom: .id)
- public var id: Int?
-
- @Parent(key: "device_id")
- public var device: DeviceType
-
- @Parent(key: "pass_id")
- public var pass: PassType
-
- public init() {}
-}
-
-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: DeviceType.schema, .id, onDelete: .cascade)
- .foreignKey("pass_id", references: PassType.schema, .id, onDelete: .cascade)
- .create()
- }
-
- public func revert(on database: any Database) async throws {
- try await database.schema(Self.schema).delete()
- }
-}
diff --git a/Sources/Passes/Models/PassKitPassData.swift b/Sources/Passes/Models/PassDataModel.swift
similarity index 93%
rename from Sources/Passes/Models/PassKitPassData.swift
rename to Sources/Passes/Models/PassDataModel.swift
index cf419a2..7da4280 100644
--- a/Sources/Passes/Models/PassKitPassData.swift
+++ b/Sources/Passes/Models/PassDataModel.swift
@@ -26,18 +26,17 @@
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
-import Vapor
-import Fluent
+import FluentKit
/// Represents the `Model` that stores custom app data associated to PassKit passes.
-public protocol PassKitPassData: Model {
- associatedtype PassType: PassKitPass
+public protocol PassDataModel: Model {
+ associatedtype PassType: PassModel
/// The foreign key to the pass table
var pass: PassType { get set }
}
-internal extension PassKitPassData {
+internal extension PassDataModel {
var _$pass: Parent {
guard let mirror = Mirror(reflecting: self).descendant("_pass"),
let pass = mirror as? Parent else {
diff --git a/Sources/Passes/Models/PassKitPass.swift b/Sources/Passes/Models/PassModel.swift
similarity index 80%
rename from Sources/Passes/Models/PassKitPass.swift
rename to Sources/Passes/Models/PassModel.swift
index 6f4e174..430b035 100644
--- a/Sources/Passes/Models/PassKitPass.swift
+++ b/Sources/Passes/Models/PassModel.swift
@@ -26,19 +26,24 @@
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
-import Vapor
-import Fluent
+import Foundation
+import FluentKit
-/// 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 {
+/// Represents the `Model` that stores PassKit passes.
+///
+/// Uses a UUID so people can't easily guess pass IDs
+public protocol PassModel: Model where IDValue == UUID {
/// The pass type identifier.
var passTypeIdentifier: String { get set }
/// The last time the pass was modified.
var updatedAt: Date? { get set }
+
+ /// The authentication token for the pass.
+ var authenticationToken: String { get set }
}
-internal extension PassKitPass {
+internal extension PassModel {
var _$id: ID {
guard let mirror = Mirror(reflecting: self).descendant("_id"),
let id = mirror as? ID else {
@@ -65,4 +70,13 @@ internal extension PassKitPass {
return updatedAt
}
+
+ var _$authenticationToken: Field {
+ guard let mirror = Mirror(reflecting: self).descendant("_authenticationToken"),
+ let authenticationToken = mirror as? Field else {
+ fatalError("authenticationToken property must be declared using @Field")
+ }
+
+ return authenticationToken
+ }
}
diff --git a/Sources/Passes/Models/PassKitRegistration.swift b/Sources/Passes/Models/PassesRegistrationModel.swift
similarity index 90%
rename from Sources/Passes/Models/PassKitRegistration.swift
rename to Sources/Passes/Models/PassesRegistrationModel.swift
index 18c53ac..91310b5 100644
--- a/Sources/Passes/Models/PassKitRegistration.swift
+++ b/Sources/Passes/Models/PassesRegistrationModel.swift
@@ -26,13 +26,13 @@
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
-import Vapor
-import Fluent
+import FluentKit
+import PassKit
-/// Represents the `Model` that stores PassKit registrations.
-public protocol PassKitRegistration: Model where IDValue == Int {
- associatedtype PassType: PassKitPass
- associatedtype DeviceType: PassKitDevice
+/// Represents the `Model` that stores passes registrations.
+public protocol PassesRegistrationModel: Model where IDValue == Int {
+ associatedtype PassType: PassModel
+ associatedtype DeviceType: DeviceModel
/// The device for this registration.
var device: DeviceType { get set }
@@ -41,7 +41,7 @@ public protocol PassKitRegistration: Model where IDValue == Int {
var pass: PassType { get set }
}
-internal extension PassKitRegistration {
+internal extension PassesRegistrationModel {
var _$device: Parent {
guard let mirror = Mirror(reflecting: self).descendant("_device"),
let device = mirror as? Parent else {
diff --git a/Sources/Passes/PassesDelegate.swift b/Sources/Passes/PassesDelegate.swift
index 4154954..ca57ec5 100644
--- a/Sources/Passes/PassesDelegate.swift
+++ b/Sources/Passes/PassesDelegate.swift
@@ -26,8 +26,8 @@
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
-import Vapor
-import Fluent
+import Foundation
+import FluentKit
/// The delegate which is responsible for generating the pass files.
public protocol PassesDelegate: AnyObject, Sendable {
@@ -39,13 +39,13 @@ public protocol PassesDelegate: AnyObject, Sendable {
/// - `signature`
///
/// - Parameters:
- /// - pass: The pass data from the SQL server.
+ /// - for: The pass data from the SQL server.
/// - db: The SQL database to query against.
///
/// - Returns: A `URL` which points to the template data for the pass.
///
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor.
- func template(for: P, db: any Database) async throws -> URL
+ func template(for: P, db: any Database) async throws -> URL
/// Generates the SSL `signature` file.
///
@@ -68,8 +68,8 @@ public protocol PassesDelegate: AnyObject, Sendable {
/// - encoder: The `JSONEncoder` which you should use.
/// - Returns: The encoded pass JSON data.
///
- /// > Tip: See the [Pass](https://developer.apple.com/documentation/walletpasses/pass) object to understand the keys.
- func encode(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data
+ /// > Tip: See the [`Pass`](https://developer.apple.com/documentation/walletpasses/pass) object to understand the keys.
+ func encode(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data
/// Should return a `URL` which points to the template data for the pass.
///
diff --git a/Sources/Passes/PassKitError.swift b/Sources/Passes/PassesError.swift
similarity index 87%
rename from Sources/Passes/PassKitError.swift
rename to Sources/Passes/PassesError.swift
index 1828a39..673211a 100644
--- a/Sources/Passes/PassKitError.swift
+++ b/Sources/Passes/PassesError.swift
@@ -1,13 +1,11 @@
//
-// File.swift
-//
+// PassesError.swift
+// PassKit
//
// Created by Scott Grosch on 1/22/20.
//
-import Foundation
-
-public enum PassKitError: Error {
+public enum PassesError: Error {
/// The template path is not a directory
case templateNotDirectory
diff --git a/Sources/Passes/PassesService.swift b/Sources/Passes/PassesService.swift
new file mode 100644
index 0000000..e34cc09
--- /dev/null
+++ b/Sources/Passes/PassesService.swift
@@ -0,0 +1,102 @@
+/// 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 FluentKit
+
+/// The main class that handles PassKit passes.
+public final class PassesService: Sendable {
+ private let service: PassesServiceCustom
+
+ public init(app: Application, delegate: any PassesDelegate, logger: Logger? = nil) {
+ service = .init(app: app, delegate: delegate, logger: logger)
+ }
+
+ /// Registers all the routes required for PassKit to work.
+ public func registerRoutes() {
+ service.registerRoutes()
+ }
+
+ /// 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 service.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 service.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(PassesDevice())
+ migrations.add(PassesRegistration())
+ migrations.add(PassesErrorLog())
+ }
+
+ /// 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 PassesServiceCustom.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 PassesServiceCustom.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 PassesServiceCustom.sendPushNotifications(for: pass, on: db, app: app)
+ }
+}
diff --git a/Sources/Passes/Passes.swift b/Sources/Passes/PassesServiceCustom.swift
similarity index 71%
rename from Sources/Passes/Passes.swift
rename to Sources/Passes/PassesServiceCustom.swift
index 13277ce..1a61918 100644
--- a/Sources/Passes/Passes.swift
+++ b/Sources/Passes/PassesServiceCustom.swift
@@ -1,125 +1,30 @@
-/// 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.
+//
+// PassesServiceCustom.swift
+// PassKit
+//
+// Created by Francesco Paolo Severino on 29/06/24.
+//
-import Vapor
+@preconcurrency import Vapor
import APNS
import VaporAPNS
@preconcurrency import APNSCore
import Fluent
import NIOSSL
+import PassKit
-/// The main class that handles PassKit passes.
-public final class Passes: Sendable {
- private let kit: PassesCustom
-
- public init(app: Application, delegate: any PassesDelegate, logger: Logger? = nil) {
- kit = .init(app: app, delegate: delegate, logger: logger)
- }
-
- /// Registers all the routes required for PassKit to work.
- ///
- /// - Parameter authorizationCode: The `authenticationToken` which you are going to use in the `pass.json` file.
- public func registerRoutes(authorizationCode: String? = nil) {
- 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())
- migrations.add(PKRegistration())
- migrations.add(PKErrorLog())
- }
-
- /// 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 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 PassesCustom.sendPushNotifications(for: pass, on: db, app: app)
- }
-}
-
-/// Class to handle `Passes`.
+/// Class to handle `PassesService`.
///
/// The generics should be passed in this order:
/// - Pass Type
/// - Device Type
/// - Registration Type
/// - Error Log Type
-public final class PassesCustom: Sendable where P == R.PassType, D == R.DeviceType {
+public final class PassesServiceCustom
: 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 v1: any RoutesBuilder
private let logger: Logger?
public init(app: Application, delegate: any PassesDelegate, logger: Logger? = nil) {
@@ -127,21 +32,15 @@ public final class PassesCustom())
v1auth.post("devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier", ":passSerial", use: { try await self.registerDevice(req: $0) })
v1auth.get("passes", ":passTypeIdentifier", ":passSerial", use: { try await self.latestVersionOfPass(req: $0) })
@@ -152,23 +51,23 @@ public final class PassesCustom
HTTPStatus {
logger?.debug("Called register device")
@@ -406,8 +307,10 @@ public final class PassesCustom