top of page

GiveHub Check-In iPad App — Keep Existing UI, Add Native Silent Printing

We do not want to rebuild the whole check-in feature for iPad.

We already have a strong check-in UI/flow. The goal is to reuse the existing check-in frontend and make it work on iPad with silent Brother printing.

Goal

Keep the existing GiveHub check-in UI and logic as intact as possible, but package it inside a native iPad app that adds:

  • silent printing to Brother QL-820NWB

  • no iOS print dialog

  • saved/default printer per iPad

  • optional QR scanning

  • kiosk-style app behavior

Required architecture

Build a native iPad wrapper app in Swift/SwiftUI that uses a WKWebView to load the existing GiveHub check-in web experience.

The native app should be responsible for:

  • loading the existing check-in UI in a webview

  • storing station/device config locally

  • storing the selected Brother printer locally

  • receiving “print this label now” commands from the web layer

  • printing silently through Brother’s native iOS SDK

  • handling printer status/errors

  • optionally handling camera QR scanning later

The existing GiveHub web check-in UI should remain responsible for:

  • phone number entry

  • family lookup

  • child selection

  • check-in flow

  • existing validation

  • existing screen design and UX

  • calling backend APIs as it already does

Important

Do not rebuild the current check-in screens in native unless absolutely necessary.

Instead, use a hybrid structure:

Native iPad app shell
→ hosts existing GiveHub check-in UI in WKWebView
→ web UI sends print requests to native bridge
→ native code prints silently to Brother printer

Why this approach

Brother’s official mobile SDK supports the QL-820NWB and supports Wi-Fi printing on iOS. Brother’s iOS setup docs also say that if we do not use Bluetooth Classic, we can ship using the WLAN-only framework path and do not need a Product Plan ID (PPID) for App Store publishing. That makes Wi-Fi-first the correct first implementation.

 

V1 recommendation

Use:

  • native iPad shell

  • WKWebView for the current GiveHub UI

  • Brother iOS SDK

  • Wi-Fi printing only

  • QL-820NWB as the initial supported printer

  • backend-rendered label images for printing

Do not use:

  • AirPrint

  • Safari printing

  • browser print dialogs

  • share-sheet printing

 

Web-to-native bridge

Use a WKWebView JavaScript bridge.

The web app should call a native message handler when it needs printing.

Example concept:

 

In web layer

When a check-in completes successfully, the web UI sends a message like:

{
"type": "printLabels",
"sessionId": "chk_123",
"labels": [
{
"labelId": "lbl_1",
"imageUrl": "https://.../lbl_1.png",
"sequence": 1
},
{
"labelId": "lbl_2",
"imageUrl": "https://.../lbl_2.png",
"sequence": 2
}
]
}

 

In native layer

The native app listens for this bridge message, downloads the label images, and prints them silently through Brother SDK to the saved printer.

Use Apple’s WKWebView script message handler pattern for this bridge.

Native responsibilities

Leap should build these native-only pieces:

 

1. Native app shell

  • Swift / SwiftUI iPad app

  • launches directly into GiveHub check-in web UI

  • kiosk-safe app wrapper

 

2. WKWebView bridge

  • exposes native handlers like:

    • printLabels

    • printerStatus

    • configurePrinter

    • testPrint

  • sends results back to web UI if needed

 

3. Printer setup screen

A hidden/admin-only native screen where staff can:

  • choose or manually enter printer IP

  • save default printer

  • run test print

  • assign station name

  • confirm printer status

 

4. Brother print service

Native service that:

  • uses Brother iOS SDK

  • connects by Wi-Fi to saved QL-820NWB

  • prints label images silently

  • checks printer state

  • returns structured errors

 

5. Optional QR scanner later

Can be native camera scanning that passes token into the existing web flow.

Existing web UI changes only

We want minimal changes to the existing check-in frontend.

Leap should only add the minimum needed to support the native bridge:

 

Add a small platform adapter in the web app

The existing web app should detect:

  • if running in normal browser → keep current behavior

  • if running inside GiveHub iPad app → send print requests to native instead of browser print flow

Example:

if GiveHubNativeApp exists:
send labels to native bridge
else:
use existing browser/desktop print behavior

 

Best implementation pattern

Create a tiny frontend wrapper like:

  • frontend/lib/native-checkin-bridge.ts

Responsibilities:

  • detect native iPad shell

  • send print payload to native

  • optionally ask native for printer status

  • gracefully fall back to web behavior outside app

 

Printer payload format

Leap should standardize a simple JSON payload from web → native:

{
"type": "printLabels",
"jobId": "printjob_123",
"sessionId": "chk_123",
"labels": [
{
"labelId": "lbl_child_1",
"labelType": "child",
"imageUrl": "https://api.givehub.com/labels/lbl_child_1.png",
"sequence": 1,
"copies": 1
},
{
"labelId": "lbl_guardian_1",
"labelType": "guardian",
"imageUrl": "https://api.givehub.com/labels/lbl_guardian_1.png",
"sequence": 2,
"copies": 1
}
]
}

Native prints in sequence order.

 

Label rendering

Do not rebuild label layout in the iPad app for V1.

Instead:

  • keep label rendering on backend

  • backend returns final Brother-ready image URLs

  • native app prints those images through Brother SDK

This keeps all current label logic centralized and avoids duplicating label layout rules in Swift.

 

Native file/module suggestions

Suggested native files:

iPadApp/
App/
GiveHubCheckInApp.swift
AppCoordinator.swift

Web/
CheckInWebView.swift
NativeBridgeController.swift
NativeBridgeModels.swift

Printer/
LabelPrinterService.swift
BrotherWiFiPrinterService.swift
PrinterSetupView.swift
PrinterConfigStore.swift
PrinterStatusModels.swift

Admin/
StationSettingsView.swift
HiddenAdminEntryView.swift

 

Web file/module suggestions

Suggested minimal web additions:

frontend/lib/native-checkin-bridge.ts
frontend/lib/platform-detect.ts
frontend/features/checkin/print-via-native.ts

 

Native bridge methods

Please expose native methods like:

  • printLabels(payload)

  • getPrinterStatus()

  • testPrint()

  • openPrinterSetup()

Optional callback/event messages back to web:

  • printSuccess

  • printFailure

  • printerOffline

 

Printer setup

Printer setup should happen once per iPad.

Each iPad should store:

  • orgId

  • stationId

  • station name

  • printer model

  • printer IP

  • label size

  • mode

Example local config:

{
"orgId": 1,
"stationId": "ipad-kiosk-01",
"stationName": "Lobby Kiosk 1",
"printer": {
"model": "Brother QL-820NWB",
"connectionType": "wifi",
"ipAddress": "192.168.1.88",
"labelSize": "62mm"
}
}

 

Error handling

If printer fails, native app should show a simple native overlay or return an error to the web UI.

Need clean handling for:

  • printer offline

  • out of labels

  • cover open

  • network timeout

  • partial print failure

 

Acceptance criteria

This project is successful if:

  1. The existing GiveHub check-in UI still runs with minimal changes.

  2. On iPad, after a successful check-in, labels print instantly with no iOS print dialog.

  3. The printer is configured once and reused automatically.

  4. The same web check-in flow can still run in browser outside the app.

  5. Native code handles Brother printing; web code does not.

  6. We do not duplicate the entire check-in feature in Swift.

 

Summary

We want a hybrid iPad app, not a rewrite.

Keep:

  • current check-in UI

  • current backend flows

  • current family/child logic

  • current label generation

Add:

  • native iPad shell

  • WKWebView bridge

  • Brother SDK printing

  • saved printer per device

  • silent printing

Do not do:

  • full native rewrite of the check-in feature

  • browser printing on iPad

  • AirPrint-based solution

     

GiveHub iPad App — WKWebView + Native Print Bridge (for Leap)

Objective

Embed the existing GiveHub Check-In web UI inside a native iPad app and allow the web layer to trigger silent Brother label printing via a native bridge.

 

File: CheckInWebView.swift

import SwiftUI import WebKit struct CheckInWebView: UIViewRepresentable { let url: URL func makeCoordinator() -> Coordinator { Coordinator() } func makeUIView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() let contentController = WKUserContentController() // Register message handlers (web → native) contentController.add(context.coordinator, name: "printLabels") contentController.add(context.coordinator, name: "getPrinterStatus") config.userContentController = contentController let webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = context.coordinator webView.load(URLRequest(url: url)) return webView } func updateUIView(_ webView: WKWebView, context: Context) {} // MARK: - Coordinator (Bridge Handler) class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate { private let printerService = BrotherWiFiPrinterService() // Receive messages from web app func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { switch message.name { case "printLabels": guard let payload = message.body as? [String: Any] else { return } handlePrintLabels(payload: payload) case "getPrinterStatus": handlePrinterStatus() default: break } } // MARK: - Print Handler private func handlePrintLabels(payload: [String: Any]) { guard let labels = payload["labels"] as? [[String: Any]] else { return } Task { do { let jobs = try await downloadLabels(labels: labels) try await printerService.printLabels(jobs) print("✅ Print success") } catch { print("❌ Print error: \(error)") // TODO: send error back to web layer if needed } } } // MARK: - Download Labels private func downloadLabels(labels: [[String: Any]]) async throws -> [PrintLabelJob] { var jobs: [PrintLabelJob] = [] // Sort by sequence to ensure correct print order let sortedLabels = labels.sorted { ($0["sequence"] as? Int ?? 0) < ($1["sequence"] as? Int ?? 0) } for label in sortedLabels { guard let urlString = label["imageUrl"] as? String, let url = URL(string: urlString), let labelId = label["labelId"] as? String else { continue } let (data, _) = try await URLSession.shared.data(from: url) let job = PrintLabelJob( labelId: labelId, labelType: label["labelType"] as? String ?? "", imageData: data, sequence: label["sequence"] as? Int ?? 0 ) jobs.append(job) } return jobs } // MARK: - Printer Status (optional) private func handlePrinterStatus() { Task { let status = await printerService.getStatus() print("Printer status: \(status)") // TODO: optionally send back to web layer } } } }

 

Supporting Model: PrintLabelJob.swift

struct PrintLabelJob { let labelId: String let labelType: String let imageData: Data let sequence: Int }

 

Printer Service Stub: BrotherWiFiPrinterService.swift

class BrotherWiFiPrinterService { func printLabels(_ labels: [PrintLabelJob]) async throws { let sorted = labels.sorted { $0.sequence < $1.sequence } for label in sorted { try await printSingle(label) } } func printSingle(_ job: PrintLabelJob) async throws { // TODO: Integrate Brother iOS SDK here // Steps: // 1. Connect to printer via saved IP // 2. Convert imageData to printable format // 3. Send to printer // 4. Handle errors (offline, no media, etc.) print("Printing label: \(job.labelId)") } func getStatus() async -> String { // TODO: Return real printer status return "connected" } }

 

Web Integration (VERY IMPORTANT)

 

Add this helper in your frontend:

export function printLabelsNative(payload: any) { if ((window as any).webkit?.messageHandlers?.printLabels) { (window as any).webkit.messageHandlers.printLabels.postMessage(payload) } else { console.log("Native bridge not available — fallback to web print") } }

 

Example Payload from Web

{ "type": "printLabels", "sessionId": "chk_123", "labels": [ { "labelId": "lbl_child_1", "labelType": "child", "imageUrl": "https://api.givehub.com/labels/lbl1.png", "sequence": 1 }, { "labelId": "lbl_guardian_1", "labelType": "guardian", "imageUrl": "https://api.givehub.com/labels/lbl2.png", "sequence": 2 } ] }

 

Summary for Leap

  • Do NOT rebuild check-in UI

  • Load existing GiveHub check-in in WKWebView

  • Use WKScriptMessageHandler bridge

  • Web sends printLabels message

  • Native downloads label images

  • Native prints via Brother SDK

  • No AirPrint, no dialogs

 

Success Criteria

  • Tap “Check In” in web UI

  • Labels print instantly on iPad

  • No iOS print popup

  • Same web UI works unchanged outside the app

A few important notes for Leap first:

  • The QL-820NWB/QL-820NWBc is supported by Brother’s Print SDK for image/PDF printing over Wi-Fi, Bluetooth, and USB.

  • Brother’s current iOS docs show the core flow as: create a BRLMChannel with the printer IP, call BRLMPrinterDriverGenerator.open(...), create BRLMQLPrintSettings, set the QL label size, then call printImageWithURL.

  • For a Wi-Fi-only app, Brother says you can use Net/BRLMPrinterKit.xcframework; for Xcode 12+ network printer apps, add NSLocalNetworkUsageDescription and Bonjour services entries to Info.plist.

Brother SDK setup for Leap

1) Xcode / framework setup

Use the Wi-Fi-only framework path:

Net/BRLMPrinterKit.xcframework

Do not start with Bluetooth. Brother’s docs explicitly allow the Wi-Fi-only framework path for apps that do not use Bluetooth Classic.

2) Info.plist

Add these keys for local network discovery / network printer access on iOS:

<key>NSLocalNetworkUsageDescription</key>
<string>GiveHub needs local network access to connect to Brother label printers on your church network.</string>

<key>NSBonjourServices</key>
<array>
<string>_pdl-datastream._tcp</string>
<string>_printer._tcp</string>
<string>_ipp._tcp</string>
</array>

That matches Brother’s iOS setup guidance for network printers.

File: BrotherWiFiPrinterService.swift

import Foundation
import UIKit
import BRLMPrinterKit

enum PrinterServiceError: LocalizedError {
case missingPrinterIP
case invalidImageData
case failedToWriteTempFile
case openChannelFailed(code: Int)
case printFailed(code: Int, message: String)
case unsupportedLabelSize
case unknownStatus(String)

var errorDescription: String? {
switch self {
case .missingPrinterIP:
return "Missing printer IP address."
case .invalidImageData:
return "Invalid label image data."
case .failedToWriteTempFile:
return "Failed to write temporary label image file."
case .openChannelFailed(let code):
return "Could not open Brother printer channel. Code: \(code)"
case .printFailed(let code, let message):
return "Brother print failed. Code: \(code). \(message)"
case .unsupportedLabelSize:
return "Unsupported Brother label size."
case .unknownStatus(let message):
return message
}
}
}

struct PrinterConfig: Codable {
let model: String
let ipAddress: String
let labelSize: String // ex: "62mm"
}

struct PrintLabelJob {
let labelId: String
let labelType: String
let imageData: Data
let sequence: Int
}

struct PrinterStatusSummary: Codable {
let connected: Bool
let message: String
}

final class BrotherWiFiPrinterService {

private let configStore = PrinterConfigStore()

// MARK: - Public API

func printLabels(_ labels: [PrintLabelJob]) async throws {
let sorted = labels.sorted { $0.sequence < $1.sequence }
for label in sorted {
try await printSingle(label)
}
}

func printSingle(_ job: PrintLabelJob) async throws {
let printerConfig = try loadPrinterConfig()

// 1) Save image to a temp file because Brother's API prints from URL
let imageURL = try writeTempImage(data: job.imageData, labelId: job.labelId)

// 2) Open printer channel over Wi-Fi
let channel = BRLMChannel(wifiIPAddress: printerConfig.ipAddress)
let generateResult = BRLMPrinterDriverGenerator.open(channel)

guard generateResult.error.code == BRLMOpenChannelErrorCode.noError,
let printerDriver = generateResult.driver else {
throw PrinterServiceError.openChannelFailed(
code: Int(generateResult.error.code.rawValue)
)
}

defer {
printerDriver.closeChannel()
try? FileManager.default.removeItem(at: imageURL)
}

// 3) Create QL print settings
let settings = BRLMQLPrintSettings(
defaultPrintSettingsWithPrinterModel: mapQLPrinterModel(printerConfig.model)
)

// 4) Set label size
settings.labelSize = try mapQLLabelSize(printerConfig.labelSize)

// Optional but recommended defaults for kiosk labels
// Exact property names can vary slightly by SDK version.
// If a property name differs in your installed framework, adapt to the SDK's current symbol.
settings.autoCut = true
settings.halftone = .threshold
settings.scaleMode = .fitPaperAspect

// 5) Print
let printError = printerDriver.printImage(with: imageURL, settings: settings)

if printError.code != BRLMPrintErrorCode.noError {
throw PrinterServiceError.printFailed(
code: Int(printError.code.rawValue),
message: printError.description ?? "Unknown Brother print error"
)
}
}

func getStatus() async -> PrinterStatusSummary {
do {
let printerConfig = try loadPrinterConfig()
let channel = BRLMChannel(wifiIPAddress: printerConfig.ipAddress)
let generateResult = BRLMPrinterDriverGenerator.open(channel)

guard generateResult.error.code == BRLMOpenChannelErrorCode.noError,
let printerDriver = generateResult.driver else {
return PrinterStatusSummary(
connected: false,
message: "Printer not reachable. Open channel failed."
)
}

defer { printerDriver.closeChannel() }

// Depending on SDK version, a richer status API may be available.
// For V1, successful channel open is already a useful health check.
return PrinterStatusSummary(
connected: true,
message: "Printer reachable."
)

} catch {
return PrinterStatusSummary(
connected: false,
message: error.localizedDescription
)
}
}

// MARK: - Helpers

private func loadPrinterConfig() throws -> PrinterConfig {
guard let config = configStore.load() else {
throw PrinterServiceError.missingPrinterIP
}
return config
}

private func writeTempImage(data: Data, labelId: String) throws -> URL {
guard UIImage(data: data) != nil else {
throw PrinterServiceError.invalidImageData
}

let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent("label_\(labelId).png")

do {
try data.write(to: tempURL, options: .atomic)
return tempURL
} catch {
throw PrinterServiceError.failedToWriteTempFile
}
}

private func mapQLPrinterModel(_ model: String) -> BRLMPrinterModel {
// Start with the printer we actually support.
// Add more models later if needed.
switch model {
case "Brother QL-820NWB", "QL-820NWB":
return .QL_820NWB
default:
return .QL_820NWB
}
}

private func mapQLLabelSize(_ labelSize: String) throws -> BRLMQLPrintSettingsLabelSize {
switch labelSize {
case "62mm":
// Verify exact enum name in installed SDK.
// Brother docs show that PT/QL image printing requires setting the paper/label size.
return .dieCutRoll62x100
default:
throw PrinterServiceError.unsupportedLabelSize
}
}
}

File: PrinterConfigStore.swift

import Foundation

final class PrinterConfigStore {
private let key = "givehub.printer.config"

func save(_ config: PrinterConfig) {
if let data = try? JSONEncoder().encode(config) {
UserDefaults.standard.set(data, forKey: key)
}
}

func load() -> PrinterConfig? {
guard let data = UserDefaults.standard.data(forKey: key) else {
return nil
}
return try? JSONDecoder().decode(PrinterConfig.self, from: data)
}

func clear() {
UserDefaults.standard.removeObject(forKey: key)
}
}

File: NativeBridgeController.swift

This is the bridge piece that receives the web payload and hands it to the Brother service.

import Foundation
import WebKit

final class NativeBridgeController: NSObject, WKScriptMessageHandler {
private let printerService = BrotherWiFiPrinterService()

func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
switch message.name {
case "printLabels":
guard let payload = message.body as? [String: Any] else { return }
handlePrintLabels(payload)

case "getPrinterStatus":
Task {
let status = await printerService.getStatus()
print("Printer status: \(status)")
}

default:
break
}
}

private func handlePrintLabels(_ payload: [String: Any]) {
guard let rawLabels = payload["labels"] as? [[String: Any]] else { return }

Task {
do {
let jobs = try await downloadLabels(rawLabels)
try await printerService.printLabels(jobs)
print("✅ Brother print success")
} catch {
print("❌ Brother print failed: \(error.localizedDescription)")
// Optional: send failure event back into JS here
}
}
}

private func downloadLabels(_ rawLabels: [[String: Any]]) async throws -> [PrintLabelJob] {
let sorted = rawLabels.sorted {
($0["sequence"] as? Int ?? 0) < ($1["sequence"] as? Int ?? 0)
}

var jobs: [PrintLabelJob] = []

for label in sorted {
guard
let urlString = label["imageUrl"] as? String,
let url = URL(string: urlString),
let labelId = label["labelId"] as? String
else { continue }

let (data, _) = try await URLSession.shared.data(from: url)

jobs.append(
PrintLabelJob(
labelId: labelId,
labelType: label["labelType"] as? String ?? "",
imageData: data,
sequence: label["sequence"] as? Int ?? 0
)
)
}

return jobs
}
}

File: CheckInWebView.swift

import SwiftUI
import WebKit

struct CheckInWebView: UIViewRepresentable {
let url: URL
let bridgeController = NativeBridgeController()

func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
let contentController = WKUserContentController()

contentController.add(bridgeController, name: "printLabels")
contentController.add(bridgeController, name: "getPrinterStatus")

config.userContentController = contentController

let webView = WKWebView(frame: .zero, configuration: config)
webView.load(URLRequest(url: url))
return webView
}

func updateUIView(_ webView: WKWebView, context: Context) {}
}

Minimal frontend bridge

Your existing check-in UI only needs a small adapter like this:

export function printLabelsNative(payload: any) {
const bridge = (window as any)?.webkit?.messageHandlers?.printLabels
if (bridge) {
bridge.postMessage(payload)
return
}

console.log("Native Brother bridge unavailable; use existing web fallback.")
}

Example payload from web → native

{
"type": "printLabels",
"sessionId": "chk_123",
"labels": [
{
"labelId": "lbl_child_1",
"labelType": "child",
"imageUrl": "https://api.givehub.com/labels/lbl_child_1.png",
"sequence": 1
},
{
"labelId": "lbl_guardian_1",
"labelType": "guardian",
"imageUrl": "https://api.givehub.com/labels/lbl_guardian_1.png",
"sequence": 2
}
]
}

Important notes for Leap

Brother’s official docs confirm:

  • use BRLMChannel(wifiIPAddress: ...)

  • open with BRLMPrinterDriverGenerator.open(...)

  • use BRLMQLPrintSettings

  • set the QL paper/label size before printing

  • print with printImageWithURL / Swift equivalent.

For printer discovery later, Brother also provides Wi-Fi discovery APIs via BRLMPrinterSearcher.startNetworkSearch(...), which would be useful for a hidden admin printer setup screen.

One thing Leap should verify

The exact enum names for:

  • BRLMPrinterModel.QL_820NWB

  • BRLMQLPrintSettingsLabelSize.dieCutRoll62x100

  • settings.autoCut

  • settings.scaleMode

  • settings.halftone

Those can differ slightly by SDK version, so the pattern above is correct, but the symbol names should be matched to the installed Brother framework headers/manual. The overall flow is straight from Brother’s official SDK docs.

Givehub.com

GiveHub.com
Acworth, GA 30101

United States
Email: sales@givehub.com
Phone: 866-933-7048

 

MISSION STATEMENT

 

To offer a robust and a superior product suite to help non-profits and churches increase giving and operate more effectively and efficiently with cutting edge technology. 

 

Yours in Christ, 
GiveHub.com Team

  • Facebook Social Icon
  • LinkedIn Social Icon
  • Instagram Social Icon

© 2026 GiveHub.com - All rights reserved - Support - Book a Demo

bottom of page