Files
ladill-mini/apps/ios/LadillMini/Core/Components/QrImage.swift
T
isaaccladandCursor ea6afb80a4
Deploy Ladill Mini / deploy (push) Successful in 35s
Add Ladill Mini native Android and iOS apps.
Ship full-featured mobile clients with auth, payments, QRs, push notifications, and iOS polish including Figtree typography, app icons, and QR previews.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 22:30:35 +00:00

140 lines
4.6 KiB
Swift

import CoreImage.CIFilterBuiltins
import SwiftUI
import UIKit
/// Renders a QR code on-device from the payment link. Offline and auth-free.
struct QrImage: View {
let content: String
var size: CGFloat = 220
private var image: UIImage? {
guard let data = content.data(using: .utf8) else { return nil }
let filter = CIFilter.qrCodeGenerator()
filter.setValue(data, forKey: "inputMessage")
filter.setValue("M", forKey: "inputCorrectionLevel")
guard let output = filter.outputImage else { return nil }
let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
let context = CIContext()
guard let cgImage = context.createCGImage(scaled, from: scaled.extent) else { return nil }
return UIImage(cgImage: cgImage).withTintColors(
dark: UIColor(LadillColors.ink),
light: UIColor(LadillColors.surface)
)
}
var body: some View {
if let image {
Image(uiImage: image)
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: size, height: size)
.accessibilityLabel("QR code")
}
}
}
/// Branded QR preview from the API, with on-device fallback.
struct PaymentQrPreview: View {
let publicUrl: String?
let previewUrl: String?
var size: CGFloat = 220
@State private var previewImage: UIImage?
@State private var loadFailed = false
var body: some View {
Group {
if let previewImage {
Image(uiImage: previewImage)
.resizable()
.interpolation(.high)
.scaledToFit()
.frame(width: size, height: size)
.accessibilityLabel("QR code preview")
} else if loadFailed, let encoded = publicUrl {
QrImage(content: encoded, size: size)
} else if publicUrl == nil && previewUrl == nil {
Color.clear.frame(width: size, height: size)
} else {
ProgressView()
.frame(width: size, height: size)
}
}
.task(id: previewUrl) {
await loadPreview()
}
}
private func loadPreview() async {
previewImage = nil
loadFailed = false
guard let previewUrl, let url = URL(string: previewUrl) else {
loadFailed = true
return
}
var request = URLRequest(url: url)
request.setValue("image/*", forHTTPHeaderField: "Accept")
if let token = await SessionStore.shared.token {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode),
let image = UIImage(data: data) else {
loadFailed = true
return
}
previewImage = image
} catch {
loadFailed = true
}
}
}
private extension UIImage {
func withTintColors(dark: UIColor, light: UIColor) -> UIImage {
guard let cgImage else { return self }
let width = cgImage.width
let height = cgImage.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else { return self }
context.interpolationQuality = .none
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let data = context.data else { return self }
let pixels = data.bindMemory(to: UInt8.self, capacity: width * height * 4)
for y in 0..<height {
for x in 0..<width {
let offset = (y * width + x) * 4
let isDark = pixels[offset] < 128
let color = isDark ? dark : light
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
pixels[offset] = UInt8(r * 255)
pixels[offset + 1] = UInt8(g * 255)
pixels[offset + 2] = UInt8(b * 255)
pixels[offset + 3] = 255
}
}
guard let tinted = context.makeImage() else { return self }
return UIImage(cgImage: tinted)
}
}