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..