← Back to list

SwiftUI: Create Your OWN Wallet Pass You Like! (With A Typescript Endpoint)

Generate some Wallet Pass dynamically for the User to add to Wallet.

Itsuki in Level Up Coding · 2025-12-02 22:49 · 62 claps · 11.1 min read
#swiftui #passkit #ios-app-development #ios-development
Open on Medium ↗
Wiki topics: FT · Fine-tuning & Adaptation 🌐 · Web Development 📱 · Mobile Development

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!

**Server or App!**

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!

  1. Create source files of the Pass, ie: some images and a pass.json containing the strings the system displays and the metadata that defines the pass, and optionally some localization information.
  2. Generate a manifest.json that contains a dictionary of the SHA1 hashes for EACH and ALL of the source files above for the pass.
  3. Create a PKCS #7 detached signature for the manifest
  4. 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