SwiftUI: Create Your OWN Wallet Pass You Like! (With A Typescript Endpoint)
Generate some Wallet Pass dynamically for the User to add to Wallet.
SwiftUI: Create Your OWN Wallet Pass You Like! (With A Typescript Endpoint)
Generate some Wallet Pass dynamically for the User to add to Wallet.

In my previous article ***Create An Apple Wallet Pass and Make An App To Distribute it***, we have checked out how we can create a wallet pass manually. However, obviously, in real world scenarios, we need to generate those on the run!
And that’s what we will be doing here!
- Create an API Endpoint for generating the wallet pass bundle based on some customizable configurations by converting those command line commands we had in my previous article ***Create An Apple Wallet Pass and Make An App To Distribute it*** to some code
- Build an App that allows the user to set some of the parameters in
pass.json, for example, the logo text, the primary fields, and etc, send the configuration to the endpoint and get a bundle! Of course, the actual usage is so that we can create a pass based on some user’s information, flight status, event schedules, and etc! But for fun, I am having the user to create a design they like!
Simple!
I assume that you have already get those pem keys ready, certificate and private keys from the Certificates.p12, as well as the WWDR intermediate certificate! Because, obviously, there is not API endpoints we can use to register or download those from Apple!
Feel free to grab the demo from my GitHub!
Let’s start!
Server (API Endpoint)
First of all, there are indeed couple libraries out there we can use to generate wallet pass bundles, typescript or swift.
However, they are either just
- wrapping around command line, or
- using some deprecated libraries, or
- containing a bunch of random stuff that we probably never use!
I meant, all we need to do is to just create some hashes, sign some files, and bundle some zips! We don’t need those unreliable (un-official) libraries for that!
Let’s do it from scratch here, only adding libraries we like, only implementing features we need!
And it gives us (at least me) a better idea of what is actually going on, what we can modify to achieve what we want, and couple little points to be careful about!
I will be sharing with you the main functions here, and you can grab the rest (full thing) from my ***GitHub***!
Little Recap
Let’s quickly review what we need to do to create a Wallet pass!
- Create source files of the Pass, ie: some images and
a
pass.jsoncontaining the strings the system displays and the metadata that defines the pass, and optionally some localization information. - Generate a
manifest.jsonthat contains a dictionary of the SHA1 hashes for EACH and ALL of the source files above for the pass. - Create a PKCS #7 detached
signaturefor the manifest - Zip the directory
Since we are having an API Endpoint, the zip will just be an in-memory Buffer and all we have to do is to send that data as the response.
Create pass.json
Let’s start with creating our pass.json here, based on some configuration parameters that we want to allow the user to customize (or you need to customize, for example, flight information).
What about the images? For those, we can either accept them as part of the POST request body, or use some pre-added ones. Either way, we don’t need any code to create it!
export function createPassJson(configuration: PassConfigurationParameters): string {
const relevantDates = configuration.relevantDates.map((d) => createRelevantDate(d))
const barcodes = configuration.barcodeMessage ? [createBarcodeJson(configuration.barcodeMessage)] : []
const generic: Record<string, Record<string, string>[]> = {}
if (configuration.primaryField) {
generic["primaryFields"] = [createPassFieldContentJson(configuration.primaryField)]
}
if (configuration.secondaryField) {
generic["secondaryFields"] = [createPassFieldContentJson(configuration.secondaryField)]
}
const passJson = {
formatVersion: 1,
passTypeIdentifier: PASS_TYPE_IDENTIFIER,
serialNumber: v4(),
teamIdentifier: TEAM_IDENTIFIER,
organizationName: ORGANIZATION_NAME,
description: "Giftcard",
logoText: configuration.logoText,
foregroundColor: configuration.foregroundColor,
backgroundColor: configuration.backgroundColor,
barcodes: barcodes,
relevantDates: relevantDates,
generic: generic
}
console.log("`pass.json` created: ", passJson)
return JSON.stringify(passJson)
}
function createRelevantDate(str: string): Record<string, string> {
return {
date: str
}
}
function createBarcodeJson(message: string): Record<string, string> {
const format = "PKBarcodeFormatQR"
const messageEncoding = "iso-8859-1"
const json = {
message: message,
format: format,
messageEncoding: messageEncoding
}
return json
}
function createPassFieldContentJson(content: PassFieldContent): Record<string, string> {
const json: Record<string, string> = {
value: content.value,
key: v4()
}
if (content.label) {
json["label"] = content.label
}
return json
}
And here is the PassConfigurationParameters I will be customizing based on.
import { z } from 'zod'
export const PassFieldContent = z.looseObject({
value: z.string(),
label: z.string().optional(),
})
export type PassFieldContent = z.infer<typeof PassFieldContent>
export const PassConfigurationParameters = z.looseObject({
backgroundColor: z.string(),
foregroundColor: z.string(),
logoText: z.string(),
relevantDates: z.array(z.string()),
barcodeMessage: z.string().optional(),
primaryField: PassFieldContent.optional(),
secondaryField: PassFieldContent.optional()
})
export type PassConfigurationParameters = z.infer<typeof PassConfigurationParameters>
Of course, you can change or add more to the PassConfigurationParameters!
For example, if we have some gift card, for [PrimaryFields](https://developer.apple.com/documentation/walletpasses/passfields/primaryfields-data.dictionary), instead of pure string, we can use a number, and set the [currencyCode](https://developer.apple.com/documentation/walletpasses/passfieldcontent) for it.
If we have a boarding pass, we can *add semantic tags* with gate information.
If we have an event ticket, we can add [Locations](https://developer.apple.com/documentation/walletpasses/pass/locations-data.dictionary) , [parkingInformationURL](https://developer.apple.com/documentation/walletpasses/pass), and etc!
You got it!
Create Hash
Before we can create our manifest, we first need our SHA1 hashes for those images, as well as the pass.json.
import forge from 'node-forge'
// calculate sha1 hash
// bash command: openssl sha1 <filename>
export function calculateSHA1Hash(bytes: Buffer): Promise<string> {
const hash = forge.md.sha1.create()
hash.update(bytes.toString("binary"))
return hash.digest().toHex()
}
Now, if I hate random libraries that much why don’t I use the built-in crypto here?
function generateSha1Hash(bytes: Buffer) {
const hash = crypto.createHash('sha1')
hash.update(bytes.toString("binary"))
return hash.digest('hex')
}
We could! But we need ***node-forge*** anyway, to create that PKCS #7 signature!
Create Manifest
Just looping through a bunch of Buffers to create that dictionary!
function createManifest(files: { name: string, content: Buffer }[]): string {
const manifest: { [key: string]: string } = {}
for (const file of files) {
const hash = calculateSHA1Hash(file.content)
manifest[file.name] = hash
}
console.log("`manifest.json` created: ", manifest)
return JSON.stringify(manifest)
}
I won’t have any localizations in my implementation, but this function can be used even if you do! For example, if you have some localization for English, you might have something like following in the files array.
{
"name": "en.lproj\/logo.png",
"content": ...
}
Create Signature
We are ready to create a PKCS #7 detached signature for the manifest!
export async function createSignature(manifest: string): Promise<Buffer<ArrayBuffer>> {
const keysFolder = join(BASE_DIRECTORY, KEYS_FOLDER)
// private key from Certificates.p12
const privateKeyString = await readFileString(join(keysFolder, PRIVATE_KEY_NAME))
const privateKey = forge.pki.decryptRsaPrivateKey(privateKeyString, CERTIFICATE_PASSWORD)
// certificate from Certificates.p12
const certificateString = await readFileString(join(keysFolder, CERTIFICATE_NAME))
const passCertificate = forge.pki.certificateFromPem(certificateString)
// wwdr certificate
const wwdrString = await readFileString(join(keysFolder, WWDR_NAME))
const wwdrCertificate = forge.pki.certificateFromPem(wwdrString)
const p7 = forge.pkcs7.createSignedData()
p7.content = forge.util.createBuffer(manifest, "utf8")
// add wwdr as intermediate certificate
p7.addCertificate(wwdrCertificate)
p7.addCertificate(passCertificate)
p7.addSigner({
key: privateKey,
certificate: passCertificate,
digestAlgorithm: forge.pki.oids.sha1,
authenticatedAttributes: [
{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data,
},
{
type: forge.pki.oids.messageDigest,
},
{
type: forge.pki.oids.signingTime,
},
],
})
// Sign the data in detached mode
p7.sign({ detached: true })
const signature = forge.asn1.toDer(p7.toAsn1()).getBytes()
return Buffer.from(signature, "binary")
}
IMPORTANT!
For that ***WWDR intermediate certificate, we need G4! G6 won’t work! We will get a OID is not RSA*** error when trying to call forge.pki.certificateFromPem on it!

(What about G5? Haven’t tried…)
Now, this seems to be a problem like from 5 to 10 years ago and it is still not fixed… I tried to see if there are any good libraries to use, but not really!
So!
If ***node-forge*** decided not to fix this problem before G4 expires, I guess the best thing we can do is to use child-process and run those openssl commands to create the signature!
If that’s the case, when deploying this endpoint, for example, to AWS or GCP, we might want to docker it with some linux runtime!
Create Pass Bundle
We get all the blocks we need and we can create a bundle!
export async function createPKPass(images: { name: string, content: Buffer }[], configuration: PassConfigurationParameters): Promise<Buffer> {
const passJson = createPassJson(configuration)
const files: { name: string, content: Buffer }[] = [...images, {
name: PASS_JSON_NAME,
content: Buffer.from(passJson, "utf8")
}]
const manifest = createManifest(files)
const signature = await createSignature(manifest)
const filesToZip: { name: string, content: Buffer }[] = [
...images,
{
name: SIGNATURE_NAME,
content: signature
},
{
name: MANIFEST_NAME,
content: Buffer.from(manifest, "utf8")
},
{
name: PASS_JSON_NAME,
content: Buffer.from(passJson, "utf8")
},
]
const zip: Buffer = await createZip(filesToZip)
return zip
}
export async function createZip(files: { name: string, content: Buffer }[]): Promise<Buffer> {
const passThrough = new PassThrough()
const archive = archiver('zip', {
zlib: { level: 0 } // we don't actually need to compress it but only bundling it.
})
archive.pipe(passThrough)
for (const file of files) {
archive.append(file.content, { name: file.name })
}
await archive.finalize()
return new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = []
passThrough.on('data', chunk => chunks.push(chunk))
passThrough.on('end', () => resolve(Buffer.concat(chunks)))
passThrough.on('error', reject)
})
}
If you have localizations, you might want to add those in as a function argument, as well as to the files array for creating the manifest!
Endpoint
To finish up our server, we will have a single POST endpoint to take in some PassConfigurationParameters, create the pkpass bundle (the zip) and send the data!
const app = express()
const PORT = 8080
app.use(express.json({}))
app.post("/generate", async (req: Request, res: Response) => {
const parseResult = PassConfigurationParameters.safeParse(req.body)
if (!parseResult.success) {
res.status(500).send({
error: "true",
message: parseResult.error.message
})
return
}
const configuration: PassConfigurationParameters = parseResult.data
if (!validateColor(configuration.foregroundColor)) {
res.status(500).send({
error: "true",
message: "Invalid ForegroundColor. Need to be specified as a CSS-style RGB triple, such as rgb(100, 10, 110)."
})
return
}
if (!validateColor(configuration.backgroundColor)) {
res.status(500).send({
error: "true",
message: "Invalid BackgroundColor. Need to be specified as a CSS-style RGB triple, such as rgb(100, 10, 110)."
})
return
}
const validatedDate = configuration.relevantDates.map((d) => formatDate(d))
if (!validatedDate.every((v) => v !== null)) {
res.status(500).send({
error: "true",
message: "RelevantDates Contains invalid date."
})
return
}
configuration.relevantDates = validatedDate
const assetsFolder = join(BASE_DIRECTORY, IMAGE_ASSESTS_FOLDER)
const images: { name: string, content: Buffer }[] = []
for (const imageName of IMAGE_ASSET_NAMES) {
const path = join(assetsFolder, imageName)
const bytes = await readFileAsBuffer(path)
images.push({
name: imageName,
content: bytes
})
}
const zip = await createPKPass(images, configuration)
res.set({
'Content-Type': MIME_TYPE,
'Content-disposition': `attachmentfilename=pass.pkpass`,
'Content-Length': zip.length,
})
res.status(200).send(zip)
})
app.listen(PORT, () => {
console.log(`Apple Pass Generation Server listening on port ${PORT}`)
})
process.on('SIGINT', async () => {
console.log('Shutting down server...')
process.exit(0)
})
I have some validations here but those are not really important in terms of the basic functionalities, so please let me leave those out and you can grab it from my ***GitHub*** if you like!
We can test it with insomnia, postman, or anything you like, really quick! But let’s create our App and use it to test it out instead!
App
It is simple!
Some [ColorPicker](https://developer.apple.com/documentation/swiftui/colorpicker)s, some [DatePicker](https://developer.apple.com/documentation/swiftui/datepicker)s, and some [TextField](https://developer.apple.com/documentation/swiftui/textfield)s!
import SwiftUI
import PassKit
let BASE_ENDPOINT = "http://localhost:8080"
struct GeneratePassRequest {
enum Error: Swift.Error {
case invalidURL
case timeout
case badRequest(String?)
case unknown
}
private let path: String = "/generate"
private let method = "POST"
var header: [String : String]? = nil
var body: PassConfiguration
func send() async throws -> Data {
guard let url = URL(string: "\(BASE_ENDPOINT)\(path)") else {
throw Error.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = self.method
request.allHTTPHeaderFields = self.header
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let jsonData = try JSONEncoder().encode(self.body)
request.httpBody = jsonData
var data: Data!
var response: URLResponse!
do {
(data, response) = try await URLSession.shared.data(for: request)
} catch {
throw Error.timeout
}
guard let response = response as? HTTPURLResponse else { throw Error.unknown }
let statusCode = response.statusCode
if !(200...300 ~= statusCode ) {
throw Error.badRequest(String(data: data, encoding: .utf8))
}
return data
}
}
extension Color {
var rgb: (Int, Int, Int)? {
let uiColor = UIColor(self)
var r : CGFloat = 0
var g : CGFloat = 0
var b : CGFloat = 0
if uiColor.getRed(&r, green: &g, blue: &b, alpha: nil) {
return (Int(r * 255.0), Int(g * 255.0), Int(b * 255.0))
} else {
return nil
}
}
var rgbString: String? {
guard let rgb = self.rgb else {
return nil
}
return String("rgb(\(rgb.0), \(rgb.1), \(rgb.2))")
}
static func rgb(_ red: Int, _ green: Int, _ blue: Int, _ alpha: CGFloat = 1) -> Color {
let uiColor = UIColor(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: alpha)
return Color(uiColor: uiColor)
}
}
extension Date {
static let formatter = ISO8601DateFormatter()
var isoString: String {
return Date.formatter.string(from: self)
}
}
struct PassConfiguration: Encodable {
private static let defaultForegroundColor = "rgb(231, 222, 175)"
private static let defaultBackgroundColor = "rgb(0, 126, 110)"
var backgroundColor: Color
var foregroundColor: Color
var logoText: String
var relevantDate: Date
var barcodeMessage: String
var primaryField: PassFieldContent
var secondaryField: PassFieldContent
enum CodingKey: String, Swift.CodingKey {
case backgroundColor
case foregroundColor
case logoText
case relevantDates
case barcodeMessage
case primaryField
case secondaryField
}
init() {
self.foregroundColor = Color.rgb(231, 222, 175)
self.backgroundColor = Color.rgb(0, 126, 110)
self.logoText = "Gift For You"
self.relevantDate = Date()
self.barcodeMessage = "Have a nice day!"
self.primaryField = .init()
self.secondaryField = .init()
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKey.self)
try container.encode(backgroundColor.rgbString ?? PassConfiguration.defaultBackgroundColor, forKey: .backgroundColor)
try container.encode(foregroundColor.rgbString ?? PassConfiguration.defaultForegroundColor, forKey: .foregroundColor)
try container.encode(logoText, forKey: .logoText)
try container.encode([relevantDate.isoString], forKey: .relevantDates)
try container.encode(barcodeMessage, forKey: .barcodeMessage)
if !primaryField.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
try container.encode(primaryField, forKey: .primaryField)
}
if !secondaryField.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
try container.encode(secondaryField, forKey: .secondaryField)
}
}
}
struct PassFieldContent: Encodable {
var value: String
var label: String
init() {
self.value = ""
self.label = ""
}
enum CodingKey: String, Swift.CodingKey {
case value
case label
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKey.self)
try container.encode(value.trimmingCharacters(in: .whitespacesAndNewlines), forKey: .value)
if !self.label.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
try container.encode(label, forKey: .label)
}
}
}
struct ContentView: View {
enum CreationState: Equatable {
case creating
case finish(PKPass)
case error(Error)
var title: String {
switch self {
case .creating:
"Creating"
case .finish(_):
"Finished"
case .error(_):
"Error"
}
}
static func == (lhs: CreationState, rhs: CreationState) -> Bool {
return lhs.title == rhs.title
}
}
@State private var passConfiguration = PassConfiguration()
@State private var creationState: CreationState? = nil
@Environment(\.openURL) private var openURL
var body: some View {
NavigationStack {
List {
Section {
HStack(spacing: 16) {
Image("samplePass")
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 8))
.containerRelativeFrame(.horizontal, count: 2, spacing: 16)
VStack(alignment: .leading) {
Text("Configurations")
.font(.headline)
VStack(alignment: .leading) {
Text("- Background Color")
Text("- Foreground Color")
Text("- Logo Text")
Text("- Primary Field")
Text("- Secondary Fields")
Text("- Barcode Message")
Text("- Relevant Date")
}
.font(.caption)
}
.layoutPriority(10)
}
.listRowBackground(Color.clear)
}
Section {
Text("⭐ Configurations ⭐")
.font(.title3)
.fontWeight(.bold)
.listRowBackground(Color.clear)
.frame(maxWidth: .infinity, alignment: .center)
}
.listSectionMargins(.top, 0)
Section("Colors") {
HStack {
titleText("Foreground")
ColorPicker("", selection: $passConfiguration.foregroundColor)
}
HStack {
titleText("Background")
ColorPicker("", selection: $passConfiguration.backgroundColor)
}
}
Section {
textEntryRow(title: "Logo Text", binding: $passConfiguration.logoText, placeholder: "Giftcard")
textEntryRow(title: "Primary Value", binding: $passConfiguration.primaryField.value, placeholder: "Enjoy your day")
textEntryRow(title: "Primary Label", binding: $passConfiguration.primaryField.label, placeholder: "Some Label")
textEntryRow(title: "Secondary Value", binding: $passConfiguration.secondaryField.value, placeholder: "Some other messages")
textEntryRow(title: "Secondary Label", binding: $passConfiguration.secondaryField.label, placeholder: "Some Label")
} header: {
VStack(alignment: .leading){
Text("Texts")
Text("- Leave the value empty to hide the entire field.")
.font(.caption)
Text("- Leave the label empty to hide the label.")
.font(.caption)
}
}
Section("Others") {
textEntryRow(title: "Barcode Message", binding: $passConfiguration.barcodeMessage, placeholder: "Have a nice day!")
HStack {
titleText("Relevant Date")
DatePicker("", selection: $passConfiguration.relevantDate)
}
}
}
.navigationTitle("Create Your Own Pass!")
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing, content: {
Button(action: {
self.createPass()
}, label: {
Text("Create")
})
.buttonStyle(.glassProminent)
})
})
.overlay(content: {
if let creationState = self.creationState {
progressDialog(creationState)
}
})
}
}
@ViewBuilder
private func progressDialog(_ creationState: CreationState) -> some View {
VStack(spacing: 24) {
Text(creationState.title)
.font(.title3)
.fontWeight(.bold)
Group {
switch creationState {
case .creating:
ProgressView()
.controlSize(.extraLarge)
case .finish(let pkPass):
VStack(spacing: 16) {
Text("Your pass is ready!")
AddPassToWalletButton([pkPass], onCompletion: { result in
if result, let passURL = pkPass.passURL {
openURL(passURL)
}
})
.fixedSize() // otherwise will take any available spaces
.addPassToWalletButtonStyle(.blackOutline)
}
case .error(let error):
Text(error.localizedDescription)
.foregroundStyle(.red)
}
}
.frame(minHeight: 160, alignment: .center)
}
.padding()
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .top)
.background(RoundedRectangle(cornerRadius: 8).fill(.white).fill(.yellow.opacity(0.3)))
.overlay(alignment: .topTrailing, content: {
Button(action: {
self.creationState = nil
}, label: {
Image(systemName: "xmark")
})
.disabled(self.creationState == .creating)
.foregroundStyle(.black.opacity(0.8))
.buttonStyle(.bordered)
.buttonBorderShape(.circle)
.padding()
})
.padding(.horizontal, 32)
}
@ViewBuilder
private func textEntryRow(title: String, binding: Binding<String>, placeholder: String) -> some View {
HStack {
titleText(title)
TextField(text: binding, prompt: Text(placeholder), label: {})
.multilineTextAlignment(.trailing)
.foregroundStyle(.secondary)
.font(.subheadline)
}
}
@ViewBuilder
private func titleText(_ title: String) -> some View {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
}
private func createPass() {
Task {
do {
self.creationState = .creating
let request = GeneratePassRequest(body: passConfiguration)
let data = try await request.send()
let pkPass = try PKPass(data: data)
self.creationState = .finish(pkPass)
} catch(let error) {
print(error)
self.creationState = .error(error)
}
}
}
}
Since I only have a single endpoint, I am too lazy to create a Network layer for it, but if you are interested, feel free to give my previous article Swift: Create Network Layer a check!
Thank you for reading!
That’s it for this article!
What’s next?
Pass information may get updated!
So! A web service that can register, update, and unregister a pass on a device!
Stay tuned if you are interested!
Happy customizing!
메타데이터
- post_id
- f9989c3dfba5
- slug
- swiftui-create-your-own-wallet-pass-you-like-with-a-typescript-endpoint-f9989c3dfba5
- url
- https://levelup.gitconnected.com/swiftui-create-your-own-wallet-pass-you-like-with-a-typescript-endpoint-f9989c3dfba5
- canonical_url
- https://levelup.gitconnected.com/swiftui-create-your-own-wallet-pass-you-like-with-a-typescript-endpoint-f9989c3dfba5
- author_url
- https://medium.com/@itsuki.enjoy
- status
- ok
- fetched_at
- 2026-08-12 20:06:02