From 900653c265194f8b196abe603a5e26855c43870c Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:51:10 +0100 Subject: [PATCH] Adopt `swift-wallet` and `fluent-wallet` (#19) * Update to VaporTesting * Add dependencies to `swift-wallet` and `fluent-wallet` * Make `Personalization` migration in `PassesService` optional * Improve auth middleware * Rename `PassKit` to `VaporWallet` * Change `PassKit` to `wallet` --- .github/workflows/test.yml | 1 + .spi.yml | 2 +- Package.swift | 57 ++-- README.md | 30 +- Sources/Orders/DTOs/OrderJSON.swift | 106 ------ Sources/Orders/DTOs/OrdersForDeviceDTO.swift | 11 - .../Orders/Models/Concrete Models/Order.swift | 63 ---- .../Models/Concrete Models/OrdersDevice.swift | 49 --- .../Concrete Models/OrdersErrorLog.swift | 49 --- .../Concrete Models/OrdersRegistration.swift | 51 --- Sources/Orders/Models/OrderDataModel.swift | 48 --- Sources/Orders/Models/OrderModel.swift | 67 ---- .../Models/OrdersRegistrationModel.swift | 46 --- .../Orders.docc/Extensions/OrderJSON.md | 19 -- Sources/Orders/Orders.docc/GettingStarted.md | 256 --------------- Sources/Orders/Orders.docc/Orders.md | 35 -- Sources/PassKit/DTOs/ErrorLogDTO.swift | 37 --- Sources/PassKit/DTOs/RegistrationDTO.swift | 37 --- Sources/PassKit/Models/DeviceModel.swift | 66 ---- Sources/PassKit/Models/ErrorLogModel.swift | 51 --- Sources/PassKit/WalletError.swift | 69 ---- Sources/Passes/DTOs/PassJSON.swift | 136 -------- Sources/Passes/DTOs/PassesForDeviceDTO.swift | 39 --- .../DTOs/PersonalizationDictionaryDTO.swift | 26 -- Sources/Passes/DTOs/PersonalizationJSON.swift | 66 ---- .../Passes/Models/Concrete Models/Pass.swift | 72 ---- .../Models/Concrete Models/PassesDevice.swift | 49 --- .../Concrete Models/PassesErrorLog.swift | 49 --- .../Concrete Models/PassesRegistration.swift | 51 --- .../Concrete Models/UserPersonalization.swift | 79 ----- Sources/Passes/Models/PassDataModel.swift | 94 ------ Sources/Passes/Models/PassModel.swift | 107 ------ .../Models/PassesRegistrationModel.swift | 74 ----- .../Models/UserPersonalizationModel.swift | 114 ------- .../Passes/Passes.docc/Extensions/PassJSON.md | 24 -- Sources/Passes/Passes.docc/GettingStarted.md | 307 ------------------ Sources/Passes/Passes.docc/Passes.md | 50 --- Sources/Passes/Passes.docc/Personalization.md | 84 ----- Sources/VaporWallet/DTOs/DTOs+Content.swift | 5 + .../Testing/SecretMiddleware.swift | 0 .../Testing/TestCertificate.swift | 0 .../Testing/isLoggingConfigured.swift | 0 .../VaporWallet.docc}/Resources/wallet.png | Bin .../VaporWallet.docc/VaporWallet.md} | 12 +- .../DTOs/OrderIdentifiersDTO+Content.swift | 4 + Sources/VaporWalletOrders/Exports.swift | 1 + .../Middleware/AppleOrderMiddleware.swift | 8 +- .../OrdersService+AsyncModelMiddleware.swift | 1 + .../OrdersService.swift | 10 +- .../OrdersServiceCustom.swift | 178 +++------- .../Extensions/OrdersService.md | 2 +- .../VaporWalletOrders.docc/GettingStarted.md | 144 ++++++++ .../VaporWalletOrders.md | 17 + .../VaporWalletPasses/DTOs/DTOs+Content.swift | 6 + Sources/VaporWalletPasses/Exports.swift | 1 + .../Middleware/ApplePassMiddleware.swift | 8 +- .../PassesService+AsyncModelMiddleware.swift | 1 + .../PassesService.swift | 20 +- .../PassesServiceCustom.swift | 220 ++++--------- .../Extensions/PassesService.md | 4 +- .../VaporWalletPasses.docc/GettingStarted.md | 171 ++++++++++ .../Resources/passes.png | Bin .../VaporWalletPasses.md | 25 ++ Tests/OrdersTests/Utils/OrderJSONData.swift | 51 --- .../SourceFiles}/EmptyDir/.gitkeep | 0 .../SourceFiles}/icon.png | Bin .../it-IT.lproj/pet_store_logo.png | Bin .../SourceFiles}/pet_store_logo.png | Bin .../Utils/OrderData.swift | 8 +- .../Utils/OrderJSONData.swift | 35 ++ .../Utils/withApp.swift | 5 +- .../VaporWalletOrdersTests.swift} | 55 +--- .../SourceFiles}/EmptyDir/.gitkeep | 0 .../SourceFiles}/icon.png | Bin .../SourceFiles}/it-IT.lproj/logo.png | Bin .../it-IT.lproj/personalizationLogo.png | Bin .../SourceFiles}/logo.png | Bin .../SourceFiles}/personalizationLogo.png | Bin .../Utils/PassData.swift | 17 +- .../Utils/PassJSONData.swift | 57 ++-- .../Utils/withApp.swift | 7 +- .../VaporWalletPassesTests.swift} | 71 ++-- 82 files changed, 673 insertions(+), 3042 deletions(-) delete mode 100644 Sources/Orders/DTOs/OrderJSON.swift delete mode 100644 Sources/Orders/DTOs/OrdersForDeviceDTO.swift delete mode 100644 Sources/Orders/Models/Concrete Models/Order.swift delete mode 100644 Sources/Orders/Models/Concrete Models/OrdersDevice.swift delete mode 100644 Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift delete mode 100644 Sources/Orders/Models/Concrete Models/OrdersRegistration.swift delete mode 100644 Sources/Orders/Models/OrderDataModel.swift delete mode 100644 Sources/Orders/Models/OrderModel.swift delete mode 100644 Sources/Orders/Models/OrdersRegistrationModel.swift delete mode 100644 Sources/Orders/Orders.docc/Extensions/OrderJSON.md delete mode 100644 Sources/Orders/Orders.docc/GettingStarted.md delete mode 100644 Sources/Orders/Orders.docc/Orders.md delete mode 100644 Sources/PassKit/DTOs/ErrorLogDTO.swift delete mode 100644 Sources/PassKit/DTOs/RegistrationDTO.swift delete mode 100644 Sources/PassKit/Models/DeviceModel.swift delete mode 100644 Sources/PassKit/Models/ErrorLogModel.swift delete mode 100644 Sources/PassKit/WalletError.swift delete mode 100644 Sources/Passes/DTOs/PassJSON.swift delete mode 100644 Sources/Passes/DTOs/PassesForDeviceDTO.swift delete mode 100644 Sources/Passes/DTOs/PersonalizationDictionaryDTO.swift delete mode 100644 Sources/Passes/DTOs/PersonalizationJSON.swift delete mode 100644 Sources/Passes/Models/Concrete Models/Pass.swift delete mode 100644 Sources/Passes/Models/Concrete Models/PassesDevice.swift delete mode 100644 Sources/Passes/Models/Concrete Models/PassesErrorLog.swift delete mode 100644 Sources/Passes/Models/Concrete Models/PassesRegistration.swift delete mode 100644 Sources/Passes/Models/Concrete Models/UserPersonalization.swift delete mode 100644 Sources/Passes/Models/PassDataModel.swift delete mode 100644 Sources/Passes/Models/PassModel.swift delete mode 100644 Sources/Passes/Models/PassesRegistrationModel.swift delete mode 100644 Sources/Passes/Models/UserPersonalizationModel.swift delete mode 100644 Sources/Passes/Passes.docc/Extensions/PassJSON.md delete mode 100644 Sources/Passes/Passes.docc/GettingStarted.md delete mode 100644 Sources/Passes/Passes.docc/Passes.md delete mode 100644 Sources/Passes/Passes.docc/Personalization.md create mode 100644 Sources/VaporWallet/DTOs/DTOs+Content.swift rename Sources/{PassKit => VaporWallet}/Testing/SecretMiddleware.swift (100%) rename Sources/{PassKit => VaporWallet}/Testing/TestCertificate.swift (100%) rename Sources/{PassKit => VaporWallet}/Testing/isLoggingConfigured.swift (100%) rename Sources/{PassKit/PassKit.docc => VaporWallet/VaporWallet.docc}/Resources/wallet.png (100%) rename Sources/{PassKit/PassKit.docc/PassKit.md => VaporWallet/VaporWallet.docc/VaporWallet.md} (57%) create mode 100644 Sources/VaporWalletOrders/DTOs/OrderIdentifiersDTO+Content.swift create mode 100644 Sources/VaporWalletOrders/Exports.swift rename Sources/{Orders => VaporWalletOrders}/Middleware/AppleOrderMiddleware.swift (56%) rename Sources/{Orders => VaporWalletOrders}/Middleware/OrdersService+AsyncModelMiddleware.swift (98%) rename Sources/{Orders => VaporWalletOrders}/OrdersService.swift (93%) rename Sources/{Orders => VaporWalletOrders}/OrdersServiceCustom.swift (68%) rename Sources/{Orders/Orders.docc => VaporWalletOrders/VaporWalletOrders.docc}/Extensions/OrdersService.md (78%) create mode 100644 Sources/VaporWalletOrders/VaporWalletOrders.docc/GettingStarted.md create mode 100644 Sources/VaporWalletOrders/VaporWalletOrders.docc/VaporWalletOrders.md create mode 100644 Sources/VaporWalletPasses/DTOs/DTOs+Content.swift create mode 100644 Sources/VaporWalletPasses/Exports.swift rename Sources/{Passes => VaporWalletPasses}/Middleware/ApplePassMiddleware.swift (86%) rename Sources/{Passes => VaporWalletPasses}/Middleware/PassesService+AsyncModelMiddleware.swift (98%) rename Sources/{Passes => VaporWalletPasses}/PassesService.swift (87%) rename Sources/{Passes => VaporWalletPasses}/PassesServiceCustom.swift (66%) rename Sources/{Passes/Passes.docc => VaporWalletPasses/VaporWalletPasses.docc}/Extensions/PassesService.md (61%) create mode 100644 Sources/VaporWalletPasses/VaporWalletPasses.docc/GettingStarted.md rename Sources/{Passes/Passes.docc => VaporWalletPasses/VaporWalletPasses.docc}/Resources/passes.png (100%) create mode 100644 Sources/VaporWalletPasses/VaporWalletPasses.docc/VaporWalletPasses.md delete mode 100644 Tests/OrdersTests/Utils/OrderJSONData.swift rename Tests/{OrdersTests/Templates => VaporWalletOrdersTests/SourceFiles}/EmptyDir/.gitkeep (100%) rename Tests/{OrdersTests/Templates => VaporWalletOrdersTests/SourceFiles}/icon.png (100%) rename Tests/{OrdersTests/Templates => VaporWalletOrdersTests/SourceFiles}/it-IT.lproj/pet_store_logo.png (100%) rename Tests/{OrdersTests/Templates => VaporWalletOrdersTests/SourceFiles}/pet_store_logo.png (100%) rename Tests/{OrdersTests => VaporWalletOrdersTests}/Utils/OrderData.swift (85%) create mode 100644 Tests/VaporWalletOrdersTests/Utils/OrderJSONData.swift rename Tests/{OrdersTests => VaporWalletOrdersTests}/Utils/withApp.swift (95%) rename Tests/{OrdersTests/OrdersTests.swift => VaporWalletOrdersTests/VaporWalletOrdersTests.swift} (90%) rename Tests/{PassesTests/Templates => VaporWalletPassesTests/SourceFiles}/EmptyDir/.gitkeep (100%) rename Tests/{PassesTests/Templates => VaporWalletPassesTests/SourceFiles}/icon.png (100%) rename Tests/{PassesTests/Templates => VaporWalletPassesTests/SourceFiles}/it-IT.lproj/logo.png (100%) rename Tests/{PassesTests/Templates => VaporWalletPassesTests/SourceFiles}/it-IT.lproj/personalizationLogo.png (100%) rename Tests/{PassesTests/Templates => VaporWalletPassesTests/SourceFiles}/logo.png (100%) rename Tests/{PassesTests/Templates => VaporWalletPassesTests/SourceFiles}/personalizationLogo.png (100%) rename Tests/{PassesTests => VaporWalletPassesTests}/Utils/PassData.swift (76%) rename Tests/{PassesTests => VaporWalletPassesTests}/Utils/PassJSONData.swift (50%) rename Tests/{PassesTests => VaporWalletPassesTests}/Utils/withApp.swift (93%) rename Tests/{PassesTests/PassesTests.swift => VaporWalletPassesTests/VaporWalletPassesTests.swift} (89%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbddd19..81e62e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,5 +12,6 @@ jobs: with: with_linting: true test_filter: --no-parallel + with_tsan: false secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.spi.yml b/.spi.yml index ad36f6c..b044b2d 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [PassKit, Passes, Orders] \ No newline at end of file + - documentation_targets: [VaporWallet, VaporWalletPasses, VaporWalletOrders] \ No newline at end of file diff --git a/Package.swift b/Package.swift index cdbc96a..38b924e 100644 --- a/Package.swift +++ b/Package.swift @@ -2,72 +2,71 @@ import PackageDescription let package = Package( - name: "PassKit", + name: "wallet", platforms: [ .macOS(.v13) ], products: [ - .library(name: "Passes", targets: ["Passes"]), - .library(name: "Orders", targets: ["Orders"]), + .library(name: "VaporWalletPasses", targets: ["VaporWalletPasses"]), + .library(name: "VaporWalletOrders", targets: ["VaporWalletOrders"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.108.0"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.111.0"), .package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"), + .package(url: "https://github.com/fpseverino/fluent-wallet.git", from: "0.1.0"), .package(url: "https://github.com/vapor/apns.git", from: "4.2.0"), - .package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.4"), - .package(url: "https://github.com/apple/swift-certificates.git", from: "1.6.1"), // used in tests .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.8.0"), ], targets: [ .target( - name: "PassKit", + name: "VaporWallet", dependencies: [ - .product(name: "Fluent", package: "fluent"), .product(name: "Vapor", package: "vapor"), + .product(name: "Fluent", package: "fluent"), .product(name: "VaporAPNS", package: "apns"), - .product(name: "Zip", package: "zip"), - .product(name: "X509", package: "swift-certificates"), - ], - swiftSettings: swiftSettings - ), - .target( - name: "Passes", - dependencies: [ - .target(name: "PassKit") ], swiftSettings: swiftSettings ), + // MARK: - Wallet Passes .target( - name: "Orders", + name: "VaporWalletPasses", dependencies: [ - .target(name: "PassKit") + .target(name: "VaporWallet"), + .product(name: "FluentWalletPasses", package: "fluent-wallet"), ], swiftSettings: swiftSettings ), .testTarget( - name: "PassesTests", + name: "VaporWalletPassesTests", dependencies: [ - .target(name: "Passes"), - .target(name: "PassKit"), - .product(name: "XCTVapor", package: "vapor"), + .target(name: "VaporWalletPasses"), + .product(name: "VaporTesting", package: "vapor"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), ], resources: [ - .copy("Templates") + .copy("SourceFiles") + ], + swiftSettings: swiftSettings + ), + // MARK: - Wallet Orders + .target( + name: "VaporWalletOrders", + dependencies: [ + .target(name: "VaporWallet"), + .product(name: "FluentWalletOrders", package: "fluent-wallet"), ], swiftSettings: swiftSettings ), .testTarget( - name: "OrdersTests", + name: "VaporWalletOrdersTests", dependencies: [ - .target(name: "Orders"), - .target(name: "PassKit"), - .product(name: "XCTVapor", package: "vapor"), + .target(name: "VaporWalletOrders"), + .product(name: "VaporTesting", package: "vapor"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), ], resources: [ - .copy("Templates") + .copy("SourceFiles") ], swiftSettings: swiftSettings ), diff --git a/README.md b/README.md index 4ec7ae8..2d0259b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@
())
@@ -128,7 +128,7 @@ extension PassesServiceCustom {
let pushToken: String
do {
- pushToken = try req.content.decode(RegistrationDTO.self).pushToken
+ pushToken = try req.content.decode(PushTokenDTO.self).pushToken
} catch {
throw Abort(.badRequest)
}
@@ -177,7 +177,7 @@ extension PassesServiceCustom {
return .created
}
- fileprivate func passesForDevice(req: Request) async throws -> PassesForDeviceDTO {
+ fileprivate func passesForDevice(req: Request) async throws -> SerialNumbersDTO {
logger?.debug("Called passesForDevice")
let deviceLibraryIdentifier = req.parameters.get("deviceLibraryIdentifier")!
@@ -200,14 +200,14 @@ extension PassesServiceCustom {
var serialNumbers: [String] = []
var maxDate = Date.distantPast
for registration in registrations {
- let pass = registration.pass
+ let pass = try await registration._$pass.get(on: req.db)
try serialNumbers.append(pass.requireID().uuidString)
if let updatedAt = pass.updatedAt, updatedAt > maxDate {
maxDate = updatedAt
}
}
- return PassesForDeviceDTO(with: serialNumbers, maxDate: maxDate)
+ return SerialNumbersDTO(with: serialNumbers, maxDate: maxDate)
}
fileprivate func latestVersionOfPass(req: Request) async throws -> Response {
@@ -276,22 +276,22 @@ extension PassesServiceCustom {
return .ok
}
- fileprivate func logError(req: Request) async throws -> HTTPStatus {
- logger?.debug("Called logError")
-
- let body: ErrorLogDTO
- do {
- body = try req.content.decode(ErrorLogDTO.self)
- } catch {
- throw Abort(.badRequest)
- }
+ fileprivate func logMessage(req: Request) async throws -> HTTPStatus {
+ if let logger {
+ let body: LogEntriesDTO
+ do {
+ body = try req.content.decode(LogEntriesDTO.self)
+ } catch {
+ throw Abort(.badRequest)
+ }
- guard !body.logs.isEmpty else {
- throw Abort(.badRequest)
+ for log in body.logs {
+ logger.notice("VaporWalletPasses: \(log)")
+ }
+ return .ok
+ } else {
+ return .badRequest
}
-
- try await body.logs.map(E.init(message:)).create(on: req.db)
- return .ok
}
fileprivate func personalizedPass(req: Request) async throws -> Response {
@@ -301,28 +301,26 @@ extension PassesServiceCustom {
throw Abort(.badRequest)
}
guard
- let pass = try await P.query(on: req.db)
+ try await P.query(on: req.db)
.filter(\._$id == id)
.filter(\._$typeIdentifier == PD.typeIdentifier)
- .first()
+ .first() != nil
else {
throw Abort(.notFound)
}
let userInfo = try req.content.decode(PersonalizationDictionaryDTO.self)
- let userPersonalization = U()
- userPersonalization.fullName = userInfo.requiredPersonalizationInfo.fullName
- userPersonalization.givenName = userInfo.requiredPersonalizationInfo.givenName
- userPersonalization.familyName = userInfo.requiredPersonalizationInfo.familyName
- userPersonalization.emailAddress = userInfo.requiredPersonalizationInfo.emailAddress
- userPersonalization.postalCode = userInfo.requiredPersonalizationInfo.postalCode
- userPersonalization.isoCountryCode = userInfo.requiredPersonalizationInfo.isoCountryCode
- userPersonalization.phoneNumber = userInfo.requiredPersonalizationInfo.phoneNumber
- try await userPersonalization.create(on: req.db)
-
- pass._$userPersonalization.id = try userPersonalization.requireID()
- try await pass.update(on: req.db)
+ let personalization = I()
+ personalization.fullName = userInfo.requiredPersonalizationInfo.fullName
+ personalization.givenName = userInfo.requiredPersonalizationInfo.givenName
+ personalization.familyName = userInfo.requiredPersonalizationInfo.familyName
+ personalization.emailAddress = userInfo.requiredPersonalizationInfo.emailAddress
+ personalization.postalCode = userInfo.requiredPersonalizationInfo.postalCode
+ personalization.isoCountryCode = userInfo.requiredPersonalizationInfo.isoCountryCode
+ personalization.phoneNumber = userInfo.requiredPersonalizationInfo.phoneNumber
+ personalization._$pass.id = id
+ try await personalization.create(on: req.db)
guard let token = userInfo.personalizationToken.data(using: .utf8) else {
throw Abort(.internalServerError)
@@ -331,7 +329,7 @@ extension PassesServiceCustom {
var headers = HTTPHeaders()
headers.add(name: .contentType, value: "application/octet-stream")
headers.add(name: .contentTransferEncoding, value: "binary")
- return try Response(status: .ok, headers: headers, body: Response.Body(data: self.signature(for: token)))
+ return try Response(status: .ok, headers: headers, body: Response.Body(data: self.builder.signature(for: token)))
}
// MARK: - Push Routes
@@ -422,72 +420,6 @@ extension PassesServiceCustom {
// MARK: - pkpass file generation
extension PassesServiceCustom {
- private func manifest(for directory: URL) throws -> Data {
- var manifest: [String: String] = [:]
-
- let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path)
- for relativePath in paths {
- let file = URL(fileURLWithPath: relativePath, relativeTo: directory)
- guard !file.hasDirectoryPath else {
- continue
- }
-
- let hash = try Insecure.SHA1.hash(data: Data(contentsOf: file))
- manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined()
- }
-
- return try encoder.encode(manifest)
- }
-
- // We use this function to sign the personalization token too.
- private func signature(for manifest: Data) throws -> Data {
- // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that.
- if let pemPrivateKeyPassword {
- guard FileManager.default.fileExists(atPath: self.openSSLURL.path) else {
- throw WalletError.noOpenSSLExecutable
- }
-
- let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
- try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
- defer { try? FileManager.default.removeItem(at: dir) }
-
- try manifest.write(to: dir.appendingPathComponent("manifest.json"))
- try self.pemWWDRCertificate.write(to: dir.appendingPathComponent("wwdr.pem"), atomically: true, encoding: .utf8)
- try self.pemCertificate.write(to: dir.appendingPathComponent("certificate.pem"), atomically: true, encoding: .utf8)
- try self.pemPrivateKey.write(to: dir.appendingPathComponent("private.pem"), atomically: true, encoding: .utf8)
-
- let process = Process()
- process.currentDirectoryURL = dir
- process.executableURL = self.openSSLURL
- process.arguments = [
- "smime", "-binary", "-sign",
- "-certfile", dir.appendingPathComponent("wwdr.pem").path,
- "-signer", dir.appendingPathComponent("certificate.pem").path,
- "-inkey", dir.appendingPathComponent("private.pem").path,
- "-in", dir.appendingPathComponent("manifest.json").path,
- "-out", dir.appendingPathComponent("signature").path,
- "-outform", "DER",
- "-passin", "pass:\(pemPrivateKeyPassword)",
- ]
- try process.run()
- process.waitUntilExit()
-
- return try Data(contentsOf: dir.appendingPathComponent("signature"))
- } else {
- let signature = try CMS.sign(
- manifest,
- signatureAlgorithm: .sha256WithRSAEncryption,
- additionalIntermediateCertificates: [
- Certificate(pemEncoded: self.pemWWDRCertificate)
- ],
- certificate: Certificate(pemEncoded: self.pemCertificate),
- privateKey: .init(pemEncoded: self.pemPrivateKey),
- signingTime: Date()
- )
- return Data(signature)
- }
- }
-
/// Generates the pass content bundle for a given pass.
///
/// - Parameters:
@@ -496,47 +428,11 @@ extension PassesServiceCustom {
///
/// - Returns: The generated pass content as `Data`.
public func build(pass: PD, on db: any Database) async throws -> Data {
- let filesDirectory = try await URL(fileURLWithPath: pass.template(on: db), isDirectory: true)
- guard
- (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
- else {
- throw WalletError.noSourceFiles
- }
-
- let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
- try FileManager.default.copyItem(at: filesDirectory, to: tempDir)
- defer { try? FileManager.default.removeItem(at: tempDir) }
-
- var files: [ArchiveFile] = []
-
- let passJSON = try await self.encoder.encode(pass.passJSON(on: db))
- try passJSON.write(to: tempDir.appendingPathComponent("pass.json"))
- files.append(ArchiveFile(filename: "pass.json", data: passJSON))
-
- // Pass Personalization
- if let personalizationJSON = try await pass.personalizationJSON(on: db) {
- let personalizationJSONData = try self.encoder.encode(personalizationJSON)
- try personalizationJSONData.write(to: tempDir.appendingPathComponent("personalization.json"))
- files.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData))
- }
-
- let manifest = try self.manifest(for: tempDir)
- files.append(ArchiveFile(filename: "manifest.json", data: manifest))
- try files.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest)))
-
- let paths = try FileManager.default.subpathsOfDirectory(atPath: filesDirectory.path)
- for relativePath in paths {
- let file = URL(fileURLWithPath: relativePath, relativeTo: tempDir)
- guard !file.hasDirectoryPath else {
- continue
- }
-
- try files.append(ArchiveFile(filename: relativePath, data: Data(contentsOf: file)))
- }
-
- let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).pkpass")
- try Zip.zipData(archiveFiles: files, zipFilePath: zipFile)
- return try Data(contentsOf: zipFile)
+ try await self.builder.build(
+ pass: pass.passJSON(on: db),
+ sourceFilesDirectoryPath: pass.sourceFilesDirectoryPath(on: db),
+ personalization: pass.personalizationJSON(on: db)
+ )
}
/// Generates a bundle of passes to enable your user to download multiple passes at once.
@@ -552,7 +448,7 @@ extension PassesServiceCustom {
/// - Returns: The bundle of passes as `Data`.
public func build(passes: [PD], on db: any Database) async throws -> Data {
guard passes.count > 1 && passes.count <= 10 else {
- throw WalletError.invalidNumberOfPasses
+ throw WalletPassesError.invalidNumberOfPasses
}
var files: [ArchiveFile] = []
diff --git a/Sources/Passes/Passes.docc/Extensions/PassesService.md b/Sources/VaporWalletPasses/VaporWalletPasses.docc/Extensions/PassesService.md
similarity index 61%
rename from Sources/Passes/Passes.docc/Extensions/PassesService.md
rename to Sources/VaporWalletPasses/VaporWalletPasses.docc/Extensions/PassesService.md
index 6073ecd..91e0668 100644
--- a/Sources/Passes/Passes.docc/Extensions/PassesService.md
+++ b/Sources/VaporWalletPasses/VaporWalletPasses.docc/Extensions/PassesService.md
@@ -1,4 +1,4 @@
-# ``Passes/PassesService``
+# ``VaporWalletPasses/PassesService``
## Topics
@@ -6,7 +6,7 @@
- ``build(pass:on:)``
- ``build(passes:on:)``
-- ``register(migrations:)``
+- ``register(migrations:withPersonalization:)``
### Push Notifications
diff --git a/Sources/VaporWalletPasses/VaporWalletPasses.docc/GettingStarted.md b/Sources/VaporWalletPasses/VaporWalletPasses.docc/GettingStarted.md
new file mode 100644
index 0000000..74a032d
--- /dev/null
+++ b/Sources/VaporWalletPasses/VaporWalletPasses.docc/GettingStarted.md
@@ -0,0 +1,171 @@
+# Getting Started with Passes
+
+Create the pass data model, build a pass for Apple Wallet and distribute it with a Vapor server.
+
+## Overview
+
+The `FluentWalletPasses` framework provides models to save all the basic information for passes, user devices and their registration to each pass.
+For all the other custom data needed to generate the pass, such as the barcodes, locations, etc., you have to create your own model and its model middleware to handle the creation and update of passes.
+The pass data model will be used to generate the `pass.json` file contents.
+
+See [`FluentWalletPasses`'s documentation on `PassDataModel`](https://swiftpackageindex.com/fpseverino/fluent-wallet/documentation/fluentwalletpasses/passdatamodel) to understand how to implement the pass data model and do it before continuing with this guide.
+
+> Important: You **must** add `api/passes/` to the `webServiceURL` key of the `PassJSON.Properties` struct.
+
+The pass you distribute to a user is a signed bundle that contains the `pass.json` file, images and optional localizations.
+The `VaporWalletPasses` framework provides the ``PassesService`` class that handles the creation of the pass JSON file and the signing of the pass bundle.
+The ``PassesService`` class also provides methods to send push notifications to all devices registered when you update a pass, and all the routes that Apple Wallet uses to retrieve passes.
+
+### Initialize the Service
+
+After creating the pass data model and the pass JSON data struct, initialize the ``PassesService`` inside the `configure.swift` file.
+This will implement all of the routes that Apple Wallet expects to exist on your server.
+
+> 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).
+
+```swift
+import Fluent
+import Vapor
+import VaporWalletPasses
+
+public func configure(_ app: Application) async throws {
+ ...
+ let passesService = try PassesService